From 572d0e3f276d53453909550d8c915933abc8c14c Mon Sep 17 00:00:00 2001 From: Michael Bai Date: Sat, 22 May 2021 00:09:54 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dparser=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E5=A4=84=E7=90=86int=E7=B1=BB=E5=9E=8B=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/drf/parsers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index acffcfef8..32f93a1bf 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -94,7 +94,7 @@ class BaseFileParser(BaseParser): new_row_data = {} serializer_fields = self.serializer_fields for k, v in row_data.items(): - if isinstance(v, list) or isinstance(v, dict) or isinstance(v, str) and k.strip() and v.strip(): + if type(v) in [list, dict, int] or (isinstance(v, str) and k.strip() and v.strip()): # 解决类似disk_info为字符串的'{}'的问题 if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField): v = str(v) From f8b4259a8c1ec9a48b6310d2a2d314391c13523f Mon Sep 17 00:00:00 2001 From: Michael Bai Date: Sat, 22 May 2021 17:00:01 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=9B=E5=BB=BA?= =?UTF-8?q?/=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7=E6=97=B6=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E7=AD=96=E7=95=A5=E7=9B=B8=E5=85=B3=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 75321 -> 75313 bytes apps/locale/zh/LC_MESSAGES/django.po | 93 ++++++++++++++------------- apps/users/models/user.py | 4 +- apps/users/serializers/user.py | 21 +++--- 4 files changed, 62 insertions(+), 56 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index b3bde6600c0bb872d8d1b75fc24aa8530be39c0b..53adc123954e272a9f8001bda791d4b7afcd7cc8 100644 GIT binary patch delta 14868 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 U`O}eW4%|N;xqfT7@696r16Ith($ delta 14867 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 O)3$8d80H(1_x}Omea-Ix diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index a64dde7cd..8f5f38858 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-05-21 11:08+0800\n" +"POT-Creation-Date: 2021-05-22 16:56+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -99,7 +99,7 @@ msgstr "动作" #: terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 #: tickets/models/comment.py:17 users/models/user.py:176 -#: users/models/user.py:738 users/models/user.py:764 +#: users/models/user.py:740 users/models/user.py:766 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -184,7 +184,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: users/templates/users/_select_user_modal.html:14 #: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:278 -#: xpack/plugins/cloud/serializers.py:65 +#: xpack/plugins/cloud/serializers.py:51 msgid "Username" msgstr "用户名" @@ -285,7 +285,7 @@ msgid "Cluster" msgstr "集群" #: applications/serializers/attrs/application_category/db.py:11 -#: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:63 +#: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:49 msgid "Host" msgstr "主机" @@ -295,7 +295,7 @@ msgstr "主机" #: applications/serializers/attrs/application_type/oracle.py:11 #: applications/serializers/attrs/application_type/pgsql.py:11 #: assets/models/asset.py:188 assets/models/domain.py:53 -#: xpack/plugins/cloud/serializers.py:64 +#: xpack/plugins/cloud/serializers.py:50 msgid "Port" msgstr "端口" @@ -325,7 +325,7 @@ msgstr "目标URL" #: xpack/plugins/change_auth_plan/models.py:68 #: xpack/plugins/change_auth_plan/models.py:190 #: xpack/plugins/change_auth_plan/models.py:285 -#: xpack/plugins/cloud/serializers.py:67 +#: xpack/plugins/cloud/serializers.py:53 msgid "Password" msgstr "密码" @@ -407,7 +407,7 @@ msgstr "激活" #: assets/models/asset.py:196 assets/models/cluster.py:19 #: assets/models/user.py:66 templates/_nav.html:44 -#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers.py:160 +#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers.py:146 msgid "Admin user" msgstr "管理用户" @@ -497,7 +497,7 @@ msgstr "创建者" #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 #: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:25 #: orgs/models.py:420 perms/models/base.py:56 users/models/group.py:18 -#: users/models/user.py:765 xpack/plugins/cloud/models.py:107 +#: users/models/user.py:767 xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -569,7 +569,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:750 +#: users/models/user.py:752 msgid "System" msgstr "系统" @@ -678,7 +678,7 @@ msgstr "ssh私钥" #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 -#: xpack/plugins/cloud/models.py:89 xpack/plugins/cloud/serializers.py:161 +#: xpack/plugins/cloud/models.py:89 xpack/plugins/cloud/serializers.py:147 msgid "Node" msgstr "节点" @@ -2100,8 +2100,8 @@ msgid "" msgstr "应用列表中包含与授权类型不同的应用。({})" #: perms/serializers/asset/permission.py:45 -#: perms/serializers/asset/permission.py:69 users/serializers/user.py:34 -#: users/serializers/user.py:82 +#: perms/serializers/asset/permission.py:69 users/serializers/user.py:33 +#: users/serializers/user.py:81 msgid "Is expired" msgstr "是否过期" @@ -2121,7 +2121,7 @@ msgstr "资产名称" msgid "System users name" msgstr "系统用户名称" -#: perms/serializers/asset/permission.py:70 users/serializers/user.py:81 +#: perms/serializers/asset/permission.py:70 users/serializers/user.py:80 msgid "Is valid" msgstr "账户是否有效" @@ -3897,11 +3897,15 @@ msgstr "用户来源" msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:746 +#: users/models/user.py:603 +msgid "Need update password" +msgstr "需要更新密码" + +#: users/models/user.py:748 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:749 +#: users/models/user.py:751 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3909,7 +3913,7 @@ msgstr "Administrator是初始的超级管理员" msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/profile.py:36 users/serializers/user.py:125 +#: users/serializers/profile.py:36 users/serializers/user.py:126 msgid "Password does not match security rules" msgstr "密码不满足安全规则" @@ -3921,76 +3925,76 @@ msgstr "新密码不能是最近 {} 次的密码" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:119 users/serializers/user.py:80 +#: users/serializers/profile.py:119 users/serializers/user.py:79 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:20 +#: users/serializers/user.py:22 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/serializers/user.py:21 +#: users/serializers/user.py:23 msgid "Set password" msgstr "设置密码" -#: users/serializers/user.py:28 xpack/plugins/change_auth_plan/models.py:61 +#: users/serializers/user.py:27 xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" msgstr "密码策略" -#: users/serializers/user.py:30 +#: users/serializers/user.py:29 msgid "MFA enabled" msgstr "是否开启多因子认证" -#: users/serializers/user.py:31 +#: users/serializers/user.py:30 msgid "MFA force enabled" msgstr "强制启用多因子认证" -#: users/serializers/user.py:32 +#: users/serializers/user.py:31 msgid "MFA level for display" msgstr "多因子认证等级(显示名称)" -#: users/serializers/user.py:33 +#: users/serializers/user.py:32 msgid "Login blocked" msgstr "登录被阻塞" -#: users/serializers/user.py:35 +#: users/serializers/user.py:34 msgid "Can update" msgstr "是否可更新" -#: users/serializers/user.py:36 +#: users/serializers/user.py:35 msgid "Can delete" msgstr "是否可删除" -#: users/serializers/user.py:39 users/serializers/user.py:87 +#: users/serializers/user.py:38 users/serializers/user.py:86 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:83 +#: users/serializers/user.py:82 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:85 +#: users/serializers/user.py:84 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:86 +#: users/serializers/user.py:85 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:88 +#: users/serializers/user.py:87 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:89 +#: users/serializers/user.py:88 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:113 +#: users/serializers/user.py:112 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:210 +#: users/serializers/user.py:211 msgid "name not unique" msgstr "名称重复" @@ -3999,7 +4003,7 @@ msgid "Security token validation" msgstr "安全令牌验证" #: users/templates/users/_base_otp.html:14 xpack/plugins/cloud/models.py:78 -#: xpack/plugins/cloud/serializers.py:159 +#: xpack/plugins/cloud/serializers.py:145 msgid "Account" msgstr "账户" @@ -4740,7 +4744,7 @@ msgstr "云服务商" msgid "Cloud account" msgstr "云账号" -#: xpack/plugins/cloud/models.py:81 xpack/plugins/cloud/serializers.py:140 +#: xpack/plugins/cloud/models.py:81 xpack/plugins/cloud/serializers.py:126 msgid "Regions" msgstr "地域" @@ -4748,7 +4752,7 @@ msgstr "地域" msgid "Hostname strategy" msgstr "主机名策略" -#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers.py:163 +#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers.py:149 msgid "Always update" msgstr "总是更新" @@ -4940,24 +4944,20 @@ msgstr "" msgid "Subscription ID" msgstr "" -#: xpack/plugins/cloud/serializers.py:49 -msgid "This field is required" -msgstr "这个字段是必填项" - -#: xpack/plugins/cloud/serializers.py:138 +#: xpack/plugins/cloud/serializers.py:124 msgid "History count" msgstr "执行次数" -#: xpack/plugins/cloud/serializers.py:139 +#: xpack/plugins/cloud/serializers.py:125 msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/serializers.py:162 +#: xpack/plugins/cloud/serializers.py:148 #: xpack/plugins/gathered_user/serializers.py:20 msgid "Periodic display" msgstr "定时执行" -#: xpack/plugins/cloud/utils.py:65 +#: xpack/plugins/cloud/utils.py:64 msgid "Account unavailable" msgstr "账户无效" @@ -5045,6 +5045,9 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#~ msgid "This field is required" +#~ msgstr "这个字段是必填项" + #~ msgid "{} is required" #~ msgstr "{} 字段是必填项" diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 97a9e3d6d..6f5b52f14 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -599,7 +599,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): auto_now_add=True, blank=True, null=True, verbose_name=_('Date password last updated') ) - need_update_password = models.BooleanField(default=False) + need_update_password = models.BooleanField( + default=False, verbose_name=_('Need update password') + ) wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128) dingtalk_id = models.CharField(null=True, default=None, unique=True, max_length=128) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index d7591360b..46e4ca64a 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -2,6 +2,7 @@ # from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ +from django.db.models import TextChoices from rest_framework import serializers from common.mixins import CommonBulkSerializerMixin @@ -17,15 +18,13 @@ __all__ = [ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): - EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user') - CUSTOM_PASSWORD = _('Set password') - PASSWORD_STRATEGY_CHOICES = ( - (0, EMAIL_SET_PASSWORD), - (1, CUSTOM_PASSWORD) - ) + class PasswordStrategy(TextChoices): + email = 'email', _('Reset link will be generated and sent to the user') + custom = 'custom', _('Set password') + password_strategy = serializers.ChoiceField( - choices=PASSWORD_STRATEGY_CHOICES, required=False, - label=_('Password strategy'), write_only=True, default=0 + choices=PasswordStrategy.choices, default=PasswordStrategy.email, required=False, + write_only=True, label=_('Password strategy') ) mfa_enabled = serializers.BooleanField(read_only=True, label=_('MFA enabled')) mfa_force_enabled = serializers.BooleanField(read_only=True, label=_('MFA force enabled')) @@ -117,9 +116,11 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): def validate_password(self, password): from ..utils import check_password_rules password_strategy = self.initial_data.get('password_strategy') - if password_strategy == '0': + if self.instance is None and password_strategy != self.PasswordStrategy.custom: + # 创建用户,使用邮件设置密码 return - if password_strategy is None and not password: + if self.instance and not password: + # 更新用户, 未设置密码 return if not check_password_rules(password): msg = _('Password does not match security rules') From 7edc9c37f8e0bb29de5c822732e388656bfa571f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Fri, 21 May 2021 11:29:20 +0800 Subject: [PATCH 3/8] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b8e86a2c7..3a3d4d47c 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向 - [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/) - [完整文档](https://docs.jumpserver.org) - [演示视频](https://www.bilibili.com/video/BV1ZV41127GB) +- [手动安装](https://github.com/jumpserver/installer) ## 组件项目 - [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目 From 33fb063f78da12e220a294b5e5ac260abe0abe75 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 24 May 2021 10:48:46 +0800 Subject: [PATCH 4/8] =?UTF-8?q?perf:=20=E6=9A=82=E6=97=B6=E7=A6=81?= =?UTF-8?q?=E7=94=A8xrdp=E5=AE=9E=E6=97=B6=E7=9B=91=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/terminal/models/session.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index 86843433e..b3202a9d9 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -109,8 +109,11 @@ class Session(OrgModelMixin): _PROTOCOL = self.PROTOCOL if self.is_finished: return False + if self.login_from == self.LOGIN_FROM.RT: + return False if self.protocol in [ - _PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP, _PROTOCOL.TELNET, _PROTOCOL.K8S + _PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP, + _PROTOCOL.TELNET, _PROTOCOL.K8S ]: return True else: From b82e9f860b60940fdb0cd3597c026d1174599234 Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 26 May 2021 15:25:02 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20users=20=E9=81=97=E6=BC=8F=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0035_auto_20210526_1100.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/users/migrations/0035_auto_20210526_1100.py diff --git a/apps/users/migrations/0035_auto_20210526_1100.py b/apps/users/migrations/0035_auto_20210526_1100.py new file mode 100644 index 000000000..4d4357a2b --- /dev/null +++ b/apps/users/migrations/0035_auto_20210526_1100.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-05-26 03:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0034_auto_20210506_1448'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='need_update_password', + field=models.BooleanField(default=False, verbose_name='Need update password'), + ), + ] From 4ef3b2630a1c866c4a634040b412ea6a3cc3ada2 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 31 May 2021 17:20:38 +0800 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=E7=AB=99=E5=86=85=E4=BF=A1=20(#618?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 添加站内信 * s * s * 添加接口 * fix * fix * 重构了一些 * 完成 * 完善 * s * s * s * s * s * s * 测试ok * 替换业务中发送消息的方式 * 修改 * s * 去掉 update 兼容 create * 添加 unread total 接口 * 调整json字段 Co-authored-by: xinwen --- .gitignore | 1 + apps/jumpserver/settings/base.py | 1 + apps/jumpserver/urls.py | 1 + apps/notifications/__init__.py | 0 apps/notifications/api/__init__.py | 2 + apps/notifications/api/notifications.py | 72 +++++++++ apps/notifications/api/site_msgs.py | 59 ++++++++ apps/notifications/apps.py | 5 + apps/notifications/backends/__init__.py | 36 +++++ apps/notifications/backends/base.py | 32 ++++ apps/notifications/backends/dingtalk.py | 20 +++ apps/notifications/backends/email.py | 14 ++ apps/notifications/backends/site_msg.py | 14 ++ apps/notifications/backends/wecom.py | 20 +++ apps/notifications/migrations/0001_initial.py | 92 ++++++++++++ apps/notifications/migrations/__init__.py | 0 apps/notifications/models/__init__.py | 2 + apps/notifications/models/notification.py | 50 ++++++ apps/notifications/models/site_msg.py | 29 ++++ apps/notifications/notifications.py | 141 +++++++++++++++++ apps/notifications/serializers/__init__.py | 2 + .../serializers/notifications.py | 29 ++++ apps/notifications/serializers/site_msgs.py | 28 ++++ apps/notifications/site_msg.py | 84 +++++++++++ apps/notifications/tests.py | 3 + apps/notifications/urls.py | 15 ++ apps/ops/apps.py | 1 + apps/ops/models/command.py | 6 +- apps/ops/notifications.py | 26 ++++ apps/ops/tasks.py | 4 +- apps/ops/utils.py | 10 -- apps/terminal/api/command.py | 10 +- apps/terminal/apps.py | 1 + apps/terminal/notifications.py | 142 ++++++++++++++++++ apps/terminal/utils.py | 72 --------- apps/users/models/user.py | 6 + 36 files changed, 936 insertions(+), 94 deletions(-) create mode 100644 apps/notifications/__init__.py create mode 100644 apps/notifications/api/__init__.py create mode 100644 apps/notifications/api/notifications.py create mode 100644 apps/notifications/api/site_msgs.py create mode 100644 apps/notifications/apps.py create mode 100644 apps/notifications/backends/__init__.py create mode 100644 apps/notifications/backends/base.py create mode 100644 apps/notifications/backends/dingtalk.py create mode 100644 apps/notifications/backends/email.py create mode 100644 apps/notifications/backends/site_msg.py create mode 100644 apps/notifications/backends/wecom.py create mode 100644 apps/notifications/migrations/0001_initial.py create mode 100644 apps/notifications/migrations/__init__.py create mode 100644 apps/notifications/models/__init__.py create mode 100644 apps/notifications/models/notification.py create mode 100644 apps/notifications/models/site_msg.py create mode 100644 apps/notifications/notifications.py create mode 100644 apps/notifications/serializers/__init__.py create mode 100644 apps/notifications/serializers/notifications.py create mode 100644 apps/notifications/serializers/site_msgs.py create mode 100644 apps/notifications/site_msg.py create mode 100644 apps/notifications/tests.py create mode 100644 apps/notifications/urls.py create mode 100644 apps/ops/notifications.py create mode 100644 apps/terminal/notifications.py diff --git a/.gitignore b/.gitignore index cb931287b..5d5eb57db 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ dump.rdb .tox .cache/ .idea/ +.vscode/ db.sqlite3 config.py config.yml diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 4a2e59062..1d4b2f995 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'applications.apps.ApplicationsConfig', 'tickets.apps.TicketsConfig', 'acls.apps.AclsConfig', + 'notifications', 'common.apps.CommonConfig', 'jms_oidc_rp', 'rest_framework', diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 687b7f2ae..510654048 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -23,6 +23,7 @@ api_v1 = [ path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), path('acls/', include('acls.urls.api_urls', namespace='api-acls')), + path('notifications/', include('notifications.urls', namespace='api-notifications')), path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()), ] diff --git a/apps/notifications/__init__.py b/apps/notifications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/notifications/api/__init__.py b/apps/notifications/api/__init__.py new file mode 100644 index 000000000..bde5ef849 --- /dev/null +++ b/apps/notifications/api/__init__.py @@ -0,0 +1,2 @@ +from .notifications import * +from .site_msgs import * diff --git a/apps/notifications/api/notifications.py b/apps/notifications/api/notifications.py new file mode 100644 index 000000000..7d176e7ae --- /dev/null +++ b/apps/notifications/api/notifications.py @@ -0,0 +1,72 @@ +from django.http import Http404 +from rest_framework.mixins import ListModelMixin, UpdateModelMixin +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status + +from common.drf.api import JmsGenericViewSet +from notifications.notifications import system_msgs +from notifications.models import SystemMsgSubscription +from notifications.backends import BACKEND +from notifications.serializers import ( + SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer +) + +__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet') + + +class BackendListView(APIView): + def get(self, request): + data = [ + { + 'name': backend, + 'name_display': backend.label + } + for backend in BACKEND + if backend.is_enable + ] + return Response(data=data) + + +class SystemMsgSubscriptionViewSet(ListModelMixin, + UpdateModelMixin, + JmsGenericViewSet): + lookup_field = 'message_type' + queryset = SystemMsgSubscription.objects.all() + serializer_classes = { + 'list': SystemMsgSubscriptionByCategorySerializer, + 'update': SystemMsgSubscriptionSerializer, + 'partial_update': SystemMsgSubscriptionSerializer + } + + def list(self, request, *args, **kwargs): + data = [] + category_children_mapper = {} + + subscriptions = self.get_queryset() + msgtype_sub_mapper = {} + for sub in subscriptions: + msgtype_sub_mapper[sub.message_type] = sub + + for msg in system_msgs: + message_type = msg['message_type'] + message_type_label = msg['message_type_label'] + category = msg['category'] + category_label = msg['category_label'] + + if category not in category_children_mapper: + children = [] + + data.append({ + 'category': category, + 'category_label': category_label, + 'children': children + }) + category_children_mapper[category] = children + + sub = msgtype_sub_mapper[message_type] + sub.message_type_label = message_type_label + category_children_mapper[category].append(sub) + + serializer = self.get_serializer(data, many=True) + return Response(data=serializer.data) diff --git a/apps/notifications/api/site_msgs.py b/apps/notifications/api/site_msgs.py new file mode 100644 index 000000000..e64ac23e2 --- /dev/null +++ b/apps/notifications/api/site_msgs.py @@ -0,0 +1,59 @@ +from rest_framework.response import Response +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.decorators import action + +from common.permissions import IsValidUser +from common.const.http import GET, PATCH, POST +from common.drf.api import JmsGenericViewSet +from ..serializers import ( + SiteMessageListSerializer, SiteMessageRetrieveSerializer, SiteMessageIdsSerializer, + SiteMessageSendSerializer, +) +from ..site_msg import SiteMessage + +__all__ = ('SiteMessageViewSet', ) + + +class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JmsGenericViewSet): + permission_classes = (IsValidUser,) + serializer_classes = { + 'retrieve': SiteMessageRetrieveSerializer, + 'unread': SiteMessageListSerializer, + 'list': SiteMessageListSerializer, + 'mark_as_read': SiteMessageIdsSerializer, + 'send': SiteMessageSendSerializer, + } + + def get_queryset(self): + user = self.request.user + msgs = SiteMessage.get_user_all_msgs(user.id) + return msgs + + @action(methods=[GET], detail=False) + def unread(self, request, **kwargs): + user = request.user + msgs = SiteMessage.get_user_unread_msgs(user.id) + msgs = self.filter_queryset(msgs) + return self.get_paginated_response_with_query_set(msgs) + + @action(methods=[GET], detail=False, url_path='unread-total') + def unread_total(self, request, **kwargs): + user = request.user + msgs = SiteMessage.get_user_unread_msgs(user.id) + return Response(data={'total': msgs.count()}) + + @action(methods=[PATCH], detail=False) + def mark_as_read(self, request, **kwargs): + user = request.user + seri = self.get_serializer(data=request.data) + seri.is_valid(raise_exception=True) + ids = seri.validated_data['ids'] + SiteMessage.mark_msgs_as_read(user.id, ids) + return Response({'detail': 'ok'}) + + @action(methods=[POST], detail=False) + def send(self, request, **kwargs): + seri = self.get_serializer(data=request.data) + seri.is_valid(raise_exception=True) + SiteMessage.send_msg(**seri.validated_data, sender=request.user) + return Response({'detail': 'ok'}) diff --git a/apps/notifications/apps.py b/apps/notifications/apps.py new file mode 100644 index 000000000..9c260e0b1 --- /dev/null +++ b/apps/notifications/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class NotificationsConfig(AppConfig): + name = 'notifications' diff --git a/apps/notifications/backends/__init__.py b/apps/notifications/backends/__init__.py new file mode 100644 index 000000000..4e2633072 --- /dev/null +++ b/apps/notifications/backends/__init__.py @@ -0,0 +1,36 @@ +from django.utils.translation import gettext_lazy as _ +from django.db import models + +from .dingtalk import DingTalk +from .email import Email +from .site_msg import SiteMessage +from .wecom import WeCom + + +class BACKEND(models.TextChoices): + EMAIL = 'email', _('Email') + WECOM = 'wecom', _('WeCom') + DINGTALK = 'dingtalk', _('DingTalk') + SITE_MSG = 'site_msg', _('Site message') + + @property + def client(self): + client = { + self.EMAIL: Email, + self.WECOM: WeCom, + self.DINGTALK: DingTalk, + self.SITE_MSG: SiteMessage + }[self] + return client + + def get_account(self, user): + return self.client.get_account(user) + + @property + def is_enable(self): + return self.client.is_enable() + + @classmethod + def filter_enable_backends(cls, backends): + enable_backends = [b for b in backends if cls(b).is_enable] + return enable_backends diff --git a/apps/notifications/backends/base.py b/apps/notifications/backends/base.py new file mode 100644 index 000000000..67a2d5b03 --- /dev/null +++ b/apps/notifications/backends/base.py @@ -0,0 +1,32 @@ +from django.conf import settings + + +class BackendBase: + # User 表中的字段 + account_field = None + + # Django setting 中的字段名 + is_enable_field_in_settings = None + + def get_accounts(self, users): + accounts = [] + unbound_users = [] + account_user_mapper = {} + + for user in users: + account = getattr(user, self.account_field, None) + if account: + account_user_mapper[account] = user + accounts.append(account) + else: + unbound_users.append(user) + return accounts, unbound_users, account_user_mapper + + @classmethod + def get_account(cls, user): + return getattr(user, cls.account_field) + + @classmethod + def is_enable(cls): + enable = getattr(settings, cls.is_enable_field_in_settings) + return bool(enable) diff --git a/apps/notifications/backends/dingtalk.py b/apps/notifications/backends/dingtalk.py new file mode 100644 index 000000000..ef5e9a9c6 --- /dev/null +++ b/apps/notifications/backends/dingtalk.py @@ -0,0 +1,20 @@ +from django.conf import settings + +from common.message.backends.dingtalk import DingTalk as Client +from .base import BackendBase + + +class DingTalk(BackendBase): + account_field = 'dingtalk_id' + is_enable_field_in_settings = 'AUTH_DINGTALK' + + def __init__(self): + self.dingtalk = Client( + appid=settings.DINGTALK_APPKEY, + appsecret=settings.DINGTALK_APPSECRET, + agentid=settings.DINGTALK_AGENTID + ) + + def send_msg(self, users, msg): + accounts, __, __ = self.get_accounts(users) + return self.dingtalk.send_text(accounts, msg) diff --git a/apps/notifications/backends/email.py b/apps/notifications/backends/email.py new file mode 100644 index 000000000..b1cdec755 --- /dev/null +++ b/apps/notifications/backends/email.py @@ -0,0 +1,14 @@ +from django.conf import settings +from django.core.mail import send_mail + +from .base import BackendBase + + +class Email(BackendBase): + account_field = 'email' + is_enable_field_in_settings = 'EMAIL_HOST_USER' + + def send_msg(self, users, subject, message): + from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER + accounts, __, __ = self.get_accounts(users) + send_mail(subject, message, from_email, accounts) diff --git a/apps/notifications/backends/site_msg.py b/apps/notifications/backends/site_msg.py new file mode 100644 index 000000000..33032843a --- /dev/null +++ b/apps/notifications/backends/site_msg.py @@ -0,0 +1,14 @@ +from notifications.site_msg import SiteMessage as Client +from .base import BackendBase + + +class SiteMessage(BackendBase): + account_field = 'id' + + def send_msg(self, users, subject, message): + accounts, __, __ = self.get_accounts(users) + Client.send_msg(subject, message, user_ids=accounts) + + @classmethod + def is_enable(cls): + return True diff --git a/apps/notifications/backends/wecom.py b/apps/notifications/backends/wecom.py new file mode 100644 index 000000000..80b6f1a22 --- /dev/null +++ b/apps/notifications/backends/wecom.py @@ -0,0 +1,20 @@ +from django.conf import settings + +from common.message.backends.wecom import WeCom as Client +from .base import BackendBase + + +class WeCom(BackendBase): + account_field = 'wecom_id' + is_enable_field_in_settings = 'AUTH_WECOM' + + def __init__(self): + self.wecom = Client( + corpid=settings.WECOM_CORPID, + corpsecret=settings.WECOM_SECRET, + agentid=settings.WECOM_AGENTID + ) + + def send_msg(self, users, msg): + accounts, __, __ = self.get_accounts(users) + return self.wecom.send_text(accounts, msg) diff --git a/apps/notifications/migrations/0001_initial.py b/apps/notifications/migrations/0001_initial.py new file mode 100644 index 000000000..ebe79f304 --- /dev/null +++ b/apps/notifications/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# Generated by Django 3.1 on 2021-05-31 08:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('users', '0035_auto_20210526_1100'), + ] + + operations = [ + migrations.CreateModel( + name='SiteMessage', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('subject', models.CharField(max_length=1024)), + ('message', models.TextField()), + ('is_broadcast', models.BooleanField(default=False)), + ('groups', models.ManyToManyField(to='users.UserGroup')), + ('sender', models.ForeignKey(db_constraint=False, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='send_site_message', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='UserMsgSubscription', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('message_type', models.CharField(max_length=128)), + ('receive_backends', models.JSONField(default=list)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscriptions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SystemMsgSubscription', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('message_type', models.CharField(max_length=128, unique=True)), + ('receive_backends', models.JSONField(default=list)), + ('groups', models.ManyToManyField(related_name='system_msg_subscriptions', to='users.UserGroup')), + ('users', models.ManyToManyField(related_name='system_msg_subscriptions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SiteMessageUsers', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('has_read', models.BooleanField(default=False)), + ('read_at', models.DateTimeField(default=None, null=True)), + ('sitemessage', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='m2m_sitemessageusers', to='notifications.sitemessage')), + ('user', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='m2m_sitemessageusers', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='sitemessage', + name='users', + field=models.ManyToManyField(related_name='recv_site_messages', through='notifications.SiteMessageUsers', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/notifications/migrations/__init__.py b/apps/notifications/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/notifications/models/__init__.py b/apps/notifications/models/__init__.py new file mode 100644 index 000000000..dede7511d --- /dev/null +++ b/apps/notifications/models/__init__.py @@ -0,0 +1,2 @@ +from .notification import * +from .site_msg import * diff --git a/apps/notifications/models/notification.py b/apps/notifications/models/notification.py new file mode 100644 index 000000000..94bd1ad7d --- /dev/null +++ b/apps/notifications/models/notification.py @@ -0,0 +1,50 @@ +from django.db import models + +from common.db.models import JMSModel + +__all__ = ('SystemMsgSubscription', 'UserMsgSubscription') + + +class UserMsgSubscription(JMSModel): + message_type = models.CharField(max_length=128) + user = models.ForeignKey('users.User', related_name='user_msg_subscriptions', on_delete=models.CASCADE) + receive_backends = models.JSONField(default=list) + + def __str__(self): + return f'{self.message_type}' + + +class SystemMsgSubscription(JMSModel): + message_type = models.CharField(max_length=128, unique=True) + users = models.ManyToManyField('users.User', related_name='system_msg_subscriptions') + groups = models.ManyToManyField('users.UserGroup', related_name='system_msg_subscriptions') + receive_backends = models.JSONField(default=list) + + message_type_label = '' + + def __str__(self): + return f'{self.message_type}' + + def __repr__(self): + return self.__str__() + + @property + def receivers(self): + from notifications.backends import BACKEND + + users = [user for user in self.users.all()] + + for group in self.groups.all(): + for user in group.users.all(): + users.append(user) + + receive_backends = self.receive_backends + receviers = [] + + for user in users: + recevier = {'name': str(user), 'id': user.id} + for backend in receive_backends: + recevier[backend] = bool(BACKEND(backend).get_account(user)) + receviers.append(recevier) + + return receviers diff --git a/apps/notifications/models/site_msg.py b/apps/notifications/models/site_msg.py new file mode 100644 index 000000000..3e3c09baa --- /dev/null +++ b/apps/notifications/models/site_msg.py @@ -0,0 +1,29 @@ +from django.db import models + +from common.db.models import JMSModel + +__all__ = ('SiteMessageUsers', 'SiteMessage') + + +class SiteMessageUsers(JMSModel): + sitemessage = models.ForeignKey('notifications.SiteMessage', on_delete=models.CASCADE, db_constraint=False, related_name='m2m_sitemessageusers') + user = models.ForeignKey('users.User', on_delete=models.CASCADE, db_constraint=False, related_name='m2m_sitemessageusers') + has_read = models.BooleanField(default=False) + read_at = models.DateTimeField(default=None, null=True) + + +class SiteMessage(JMSModel): + subject = models.CharField(max_length=1024) + message = models.TextField() + users = models.ManyToManyField( + 'users.User', through=SiteMessageUsers, related_name='recv_site_messages' + ) + groups = models.ManyToManyField('users.UserGroup') + is_broadcast = models.BooleanField(default=False) + sender = models.ForeignKey( + 'users.User', db_constraint=False, on_delete=models.DO_NOTHING, null=True, default=None, + related_name='send_site_message' + ) + + has_read = False + read_at = None diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py new file mode 100644 index 000000000..8563fd214 --- /dev/null +++ b/apps/notifications/notifications.py @@ -0,0 +1,141 @@ +from typing import Iterable +import traceback +from itertools import chain + +from django.db.utils import ProgrammingError +from celery import shared_task + +from notifications.backends import BACKEND +from .models import SystemMsgSubscription + +__all__ = ('SystemMessage', 'UserMessage') + + +system_msgs = [] +user_msgs = [] + + +class MessageType(type): + def __new__(cls, name, bases, attrs: dict): + clz = type.__new__(cls, name, bases, attrs) + + if 'message_type_label' in attrs \ + and 'category' in attrs \ + and 'category_label' in attrs: + message_type = clz.get_message_type() + + msg = { + 'message_type': message_type, + 'message_type_label': attrs['message_type_label'], + 'category': attrs['category'], + 'category_label': attrs['category_label'], + } + if issubclass(clz, SystemMessage): + system_msgs.append(msg) + try: + if not SystemMsgSubscription.objects.filter(message_type=message_type).exists(): + sub = SystemMsgSubscription.objects.create(message_type=message_type) + clz.post_insert_to_db(sub) + except ProgrammingError as e: + if e.args[0] == 1146: + # 表不存在 + pass + else: + raise + elif issubclass(clz, UserMessage): + user_msgs.append(msg) + + return clz + + +@shared_task +def publish_task(msg): + msg.publish() + + +class Message(metaclass=MessageType): + """ + 这里封装了什么? + 封装不同消息的模板,提供统一的发送消息的接口 + - publish 该方法的实现与消息订阅的表结构有关 + - send_msg + """ + + message_type_label: str + category: str + category_label: str + + @classmethod + def get_message_type(cls): + return cls.__name__ + + def publish_async(self): + return publish_task.delay(self) + + def publish(self): + raise NotImplementedError + + def send_msg(self, users: Iterable, backends: Iterable = BACKEND): + for backend in backends: + try: + backend = BACKEND(backend) + + get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg) + msg = get_msg_method() + client = backend.client() + + if isinstance(msg, dict): + client.send_msg(users, **msg) + else: + client.send_msg(users, msg) + except: + traceback.print_exc() + + def get_common_msg(self) -> str: + raise NotImplementedError + + def get_dingtalk_msg(self) -> str: + return self.get_common_msg() + + def get_wecom_msg(self) -> str: + return self.get_common_msg() + + def get_email_msg(self) -> dict: + msg = self.get_common_msg() + return { + 'subject': msg, + 'message': msg + } + + def get_site_msg_msg(self) -> dict: + msg = self.get_common_msg() + return { + 'subject': msg, + 'message': msg + } + + +class SystemMessage(Message): + def publish(self): + subscription = SystemMsgSubscription.objects.get( + message_type=self.get_message_type() + ) + + # 只发送当前有效后端 + receive_backends = subscription.receive_backends + receive_backends = BACKEND.filter_enable_backends(receive_backends) + + users = [ + *subscription.users.all(), + *chain(*[g.users.all() for g in subscription.groups.all()]) + ] + + self.send_msg(users, receive_backends) + + @classmethod + def post_insert_to_db(cls, subscription: SystemMsgSubscription): + pass + + +class UserMessage(Message): + pass diff --git a/apps/notifications/serializers/__init__.py b/apps/notifications/serializers/__init__.py new file mode 100644 index 000000000..bde5ef849 --- /dev/null +++ b/apps/notifications/serializers/__init__.py @@ -0,0 +1,2 @@ +from .notifications import * +from .site_msgs import * diff --git a/apps/notifications/serializers/notifications.py b/apps/notifications/serializers/notifications.py new file mode 100644 index 000000000..7415d46f7 --- /dev/null +++ b/apps/notifications/serializers/notifications.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from common.drf.serializers import BulkModelSerializer +from notifications.models import SystemMsgSubscription + + +class SystemMsgSubscriptionSerializer(BulkModelSerializer): + receive_backends = serializers.ListField(child=serializers.CharField()) + + class Meta: + model = SystemMsgSubscription + fields = ( + 'message_type', 'message_type_label', + 'users', 'groups', 'receive_backends', 'receivers' + ) + read_only_fields = ( + 'message_type', 'message_type_label', 'receivers' + ) + extra_kwargs = { + 'users': {'allow_empty': True}, + 'groups': {'allow_empty': True}, + 'receive_backends': {'required': True} + } + + +class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer): + category = serializers.CharField() + category_label = serializers.CharField() + children = SystemMsgSubscriptionSerializer(many=True) diff --git a/apps/notifications/serializers/site_msgs.py b/apps/notifications/serializers/site_msgs.py new file mode 100644 index 000000000..8d76205e1 --- /dev/null +++ b/apps/notifications/serializers/site_msgs.py @@ -0,0 +1,28 @@ +from rest_framework.serializers import ModelSerializer +from rest_framework import serializers + +from ..models import SiteMessage + + +class SiteMessageListSerializer(ModelSerializer): + class Meta: + model = SiteMessage + fields = ['id', 'subject', 'has_read', 'read_at'] + + +class SiteMessageRetrieveSerializer(ModelSerializer): + class Meta: + model = SiteMessage + fields = ['id', 'subject', 'message', 'has_read', 'read_at'] + + +class SiteMessageIdsSerializer(serializers.Serializer): + ids = serializers.ListField(child=serializers.UUIDField()) + + +class SiteMessageSendSerializer(serializers.Serializer): + subject = serializers.CharField() + message = serializers.CharField() + user_ids = serializers.ListField(child=serializers.UUIDField(), required=False) + group_ids = serializers.ListField(child=serializers.UUIDField(), required=False) + is_broadcast = serializers.BooleanField(default=False) diff --git a/apps/notifications/site_msg.py b/apps/notifications/site_msg.py new file mode 100644 index 000000000..944a8ea3c --- /dev/null +++ b/apps/notifications/site_msg.py @@ -0,0 +1,84 @@ +from django.db.models import F + +from common.utils.timezone import now +from users.models import User +from .models import SiteMessage as SiteMessageModel, SiteMessageUsers + + +class SiteMessage: + + @classmethod + def send_msg(cls, subject, message, user_ids=(), group_ids=(), sender=None, is_broadcast=False): + if not any((user_ids, group_ids, is_broadcast)): + raise ValueError('No recipient is specified') + + site_msg = SiteMessageModel.objects.create( + subject=subject, message=message, + is_broadcast=is_broadcast, sender=sender + ) + + if is_broadcast: + user_ids = User.objects.all().values_list('id', flat=True) + else: + if group_ids: + site_msg.groups.add(*group_ids) + + user_ids_from_group = User.groups.through.objects.filter( + usergroup_id__in=group_ids + ).values_list('user_id', flat=True) + + user_ids = [*user_ids, *user_ids_from_group] + + site_msg.users.add(*user_ids) + + @classmethod + def get_user_all_msgs(cls, user_id): + site_msgs = SiteMessageModel.objects.filter( + m2m_sitemessageusers__user_id=user_id + ).distinct().annotate( + has_read=F('m2m_sitemessageusers__has_read'), + read_at=F('m2m_sitemessageusers__read_at') + ).order_by('-date_created') + + return site_msgs + + @classmethod + def get_user_all_msgs_count(cls, user_id): + site_msgs_count = SiteMessageModel.objects.filter( + m2m_sitemessageusers__user_id=user_id + ).distinct().count() + return site_msgs_count + + @classmethod + def get_user_unread_msgs(cls, user_id): + site_msgs = SiteMessageModel.objects.filter( + m2m_sitemessageusers__user_id=user_id, + m2m_sitemessageusers__has_read=False + ).distinct().annotate( + has_read=F('m2m_sitemessageusers__has_read'), + read_at=F('m2m_sitemessageusers__read_at') + ).order_by('-date_created') + + return site_msgs + + @classmethod + def get_user_unread_msgs_count(cls, user_id): + site_msgs_count = SiteMessageModel.objects.filter( + m2m_sitemessageusers__user_id=user_id, + m2m_sitemessageusers__has_read=False + ).distinct().count() + return site_msgs_count + + @classmethod + def mark_msgs_as_read(cls, user_id, msg_ids): + sitemsg_users = SiteMessageUsers.objects.filter( + user_id=user_id, sitemessage_id__in=msg_ids, + has_read=False + ) + + for sitemsg_user in sitemsg_users: + sitemsg_user.has_read = True + sitemsg_user.read_at = now() + + SiteMessageUsers.objects.bulk_update( + sitemsg_users, fields=('has_read', 'read_at')) diff --git a/apps/notifications/tests.py b/apps/notifications/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/notifications/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/notifications/urls.py b/apps/notifications/urls.py new file mode 100644 index 000000000..ad05c4aca --- /dev/null +++ b/apps/notifications/urls.py @@ -0,0 +1,15 @@ + +from rest_framework_bulk.routes import BulkRouter +from django.urls import path + +from . import api + +app_name = 'notifications' + +router = BulkRouter() +router.register('system-msg-subscription', api.SystemMsgSubscriptionViewSet, 'system-msg-subscription') +router.register('site-message', api.SiteMessageViewSet, 'site-message') + +urlpatterns = [ + path('backends/', api.BackendListView.as_view(), name='backends') +] + router.urls diff --git a/apps/ops/apps.py b/apps/ops/apps.py index 8bdc04ce8..5133c6655 100644 --- a/apps/ops/apps.py +++ b/apps/ops/apps.py @@ -13,4 +13,5 @@ class OpsConfig(AppConfig): from orgs.utils import set_current_org set_current_org(Organization.root()) from .celery import signal_handler + from . import notifications super().ready() diff --git a/apps/ops/models/command.py b/apps/ops/models/command.py index 0a2012e73..e89520390 100644 --- a/apps/ops/models/command.py +++ b/apps/ops/models/command.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext from django.db import models -from terminal.utils import send_command_execution_alert_mail +from terminal.notifications import CommandExecutionAlert from common.utils import lazyproperty from orgs.models import Organization from orgs.mixins.models import OrgModelMixin @@ -99,12 +99,12 @@ class CommandExecution(OrgModelMixin): else: msg = _("Command `{}` is forbidden ........").format(self.command) print('\033[31m' + msg + '\033[0m') - send_command_execution_alert_mail({ + CommandExecutionAlert({ 'input': self.command, 'assets': self.hosts.all(), 'user': str(self.user), 'risk_level': 5, - }) + }).publish_async() self.result = {"error": msg} self.org_id = self.run_as.org_id self.is_finished = True diff --git a/apps/ops/notifications.py b/apps/ops/notifications.py new file mode 100644 index 000000000..61e9d5630 --- /dev/null +++ b/apps/ops/notifications.py @@ -0,0 +1,26 @@ +from django.utils.translation import gettext_lazy as _ + +from notifications.notifications import SystemMessage +from notifications.models import SystemMsgSubscription +from users.models import User + +__all__ = ('ServerPerformanceMessage',) + + +class ServerPerformanceMessage(SystemMessage): + category = 'Operations' + category_label = _('Operations') + message_type_label = _('Server performance') + + def __init__(self, path, usage): + self.path = path + self.usage = usage + + def get_common_msg(self): + msg = _("Disk used more than 80%: {} => {}").format(self.path, self.usage.percent) + return msg + + @classmethod + def post_insert_to_db(cls, subscription: SystemMsgSubscription): + admins = User.objects.filter(role=User.ROLE.ADMIN) + subscription.users.add(*admins) diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 02cc9290e..60f639668 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -20,7 +20,7 @@ from .celery.utils import ( disable_celery_periodic_task, delete_celery_periodic_task ) from .models import Task, CommandExecution, CeleryTask -from .utils import send_server_performance_mail +from .notifications import ServerPerformanceMessage logger = get_logger(__file__) @@ -143,7 +143,7 @@ def check_server_performance_period(): if path.startswith(uncheck_path): need_check = False if need_check and usage.percent > 80: - send_server_performance_mail(path, usage, usages) + ServerPerformanceMessage(path=path, usage=usage).publish() @shared_task(queue="ansible") diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 5ce4494a6..9993ea2cb 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -69,16 +69,6 @@ def update_or_create_ansible_task( return task, created -def send_server_performance_mail(path, usage, usages): - from users.models import User - subject = _("Disk used more than 80%: {} => {}").format(path, usage.percent) - message = subject - admins = User.objects.filter(role=User.ROLE.ADMIN) - recipient_list = [u.email for u in admins if u.email] - logger.info(subject) - send_mail_async(subject, message, recipient_list, html_message=message) - - def get_task_log_path(base_path, task_id, level=2): task_id = str(task_id) try: diff --git a/apps/terminal/api/command.py b/apps/terminal/api/command.py index 497e40fbe..b43910e26 100644 --- a/apps/terminal/api/command.py +++ b/apps/terminal/api/command.py @@ -4,28 +4,24 @@ import time from django.conf import settings from django.utils import timezone from django.shortcuts import HttpResponse -from rest_framework import viewsets from rest_framework import generics from rest_framework.fields import DateTimeField from rest_framework.response import Response -from rest_framework.decorators import action from django.template import loader -from common.http import is_true -from terminal.models import CommandStorage, Command +from terminal.models import CommandStorage from terminal.filters import CommandFilter from orgs.utils import current_org from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser -from common.const.http import GET from common.drf.api import JMSBulkModelViewSet from common.utils import get_logger -from terminal.utils import send_command_alert_mail from terminal.serializers import InsecureCommandAlertSerializer from terminal.exceptions import StorageInvalid from ..backends import ( get_command_storage, get_multi_command_storage, SessionCommandSerializer, ) +from ..notifications import CommandAlertMessage logger = get_logger(__name__) __all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI'] @@ -211,5 +207,5 @@ class InsecureCommandAlertAPI(generics.CreateAPIView): if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL and \ settings.SECURITY_INSECURE_COMMAND and \ settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER: - send_command_alert_mail(command) + CommandAlertMessage(command).publish_async() return Response() diff --git a/apps/terminal/apps.py b/apps/terminal/apps.py index f0cb05bf2..edaa38cef 100644 --- a/apps/terminal/apps.py +++ b/apps/terminal/apps.py @@ -10,4 +10,5 @@ class TerminalConfig(AppConfig): def ready(self): from . import signals_handler + from . import notifications return super().ready() diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py new file mode 100644 index 000000000..fb70e3535 --- /dev/null +++ b/apps/terminal/notifications.py @@ -0,0 +1,142 @@ +from django.utils.translation import gettext_lazy as _ +from django.conf import settings + +from users.models import User +from common.utils import get_logger, reverse +from notifications.notifications import SystemMessage +from terminal.models import Session, Command +from notifications.models import SystemMsgSubscription + +logger = get_logger(__name__) + +__all__ = ('CommandAlertMessage', 'CommandExecutionAlert') + +CATEGORY = 'terminal' +CATEGORY_LABEL = _('Terminal') + + +class CommandAlertMixin: + @classmethod + def post_insert_to_db(cls, subscription: SystemMsgSubscription): + """ + 兼容操作,试图用 `settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER` 的邮件地址找到 + 用户,把用户设置为默认接收者 + """ + emails = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',') + emails = [email.strip() for email in emails] + + users = User.objects.filter(email__in=emails) + subscription.users.add(*users) + + +class CommandAlertMessage(CommandAlertMixin, SystemMessage): + category = CATEGORY + category_label = CATEGORY_LABEL + message_type_label = _('Terminal command alert') + + def __init__(self, command): + self.command = command + + def _get_message(self): + command = self.command + session_obj = Session.objects.get(id=command['session']) + + message = _(""" + Command: %(command)s +
+ Asset: %(host_name)s (%(host_ip)s) +
+ User: %(user)s +
+ Level: %(risk_level)s +
+ Session: session detail +
+ """) % { + 'command': command['input'], + 'host_name': command['asset'], + 'host_ip': session_obj.asset_obj.ip, + 'user': command['user'], + 'risk_level': Command.get_risk_level_str(command['risk_level']), + 'session_detail_url': reverse('api-terminal:session-detail', + kwargs={'pk': command['session']}, + external=True, api_to_ui=True), + } + + return message + + def get_common_msg(self): + return self._get_message() + + def get_email_msg(self): + command = self.command + session_obj = Session.objects.get(id=command['session']) + + input = command['input'] + if isinstance(input, str): + input = input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ') + + subject = _("Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s") % { + 'name': command['user'], + 'login_from': session_obj.get_login_from_display(), + 'remote_addr': session_obj.remote_addr, + 'command': input + } + + message = self._get_message(command) + + return { + 'subject': subject, + 'message': message + } + + +class CommandExecutionAlert(CommandAlertMixin, SystemMessage): + category = CATEGORY + category_label = CATEGORY_LABEL + message_type_label = _('Batch command alert') + + def __init__(self, command): + self.command = command + + def _get_message(self): + command = self.command + input = command['input'] + input = input.replace('\n', '
') + + assets = ', '.join([str(asset) for asset in command['assets']]) + message = _(""" +
+ Assets: %(assets)s +
+ User: %(user)s +
+ Level: %(risk_level)s +
+ + ----------------- Commands ----------------
+ %(command)s
+ ----------------- Commands ----------------
+ """) % { + 'command': input, + 'assets': assets, + 'user': command['user'], + 'risk_level': Command.get_risk_level_str(command['risk_level']), + } + return message + + def get_common_msg(self): + return self._get_message() + + def get_email_msg(self): + command = self.command + + subject = _("Insecure Web Command Execution Alert: [%(name)s]") % { + 'name': command['user'], + } + message = self._get_message(command) + + return { + 'subject': subject, + 'message': message + } diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index b13383fba..68b09bcd0 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -68,78 +68,6 @@ def get_session_replay_url(session): return local_path, url -def send_command_alert_mail(command): - session_obj = Session.objects.get(id=command['session']) - - input = command['input'] - if isinstance(input, str): - input = input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ') - - subject = _("Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s") % { - 'name': command['user'], - 'login_from': session_obj.get_login_from_display(), - 'remote_addr': session_obj.remote_addr, - 'command': input - } - - recipient_list = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',') - message = _(""" - Command: %(command)s -
- Asset: %(host_name)s (%(host_ip)s) -
- User: %(user)s -
- Level: %(risk_level)s -
- Session: session detail -
- """) % { - 'command': command['input'], - 'host_name': command['asset'], - 'host_ip': session_obj.asset_obj.ip, - 'user': command['user'], - 'risk_level': Command.get_risk_level_str(command['risk_level']), - 'session_detail_url': reverse('api-terminal:session-detail', - kwargs={'pk': command['session']}, - external=True, api_to_ui=True), - } - logger.debug(message) - - send_mail_async.delay(subject, message, recipient_list, html_message=message) - - -def send_command_execution_alert_mail(command): - subject = _("Insecure Web Command Execution Alert: [%(name)s]") % { - 'name': command['user'], - } - input = command['input'] - input = input.replace('\n', '
') - recipient_list = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',') - - assets = ', '.join([str(asset) for asset in command['assets']]) - message = _(""" -
- Assets: %(assets)s -
- User: %(user)s -
- Level: %(risk_level)s -
- - ----------------- Commands ----------------
- %(command)s
- ----------------- Commands ----------------
- """) % { - 'command': input, - 'assets': assets, - 'user': command['user'], - 'risk_level': Command.get_risk_level_str(command['risk_level']), - } - - send_mail_async.delay(subject, message, recipient_list, html_message=message) - - class ComputeStatUtil: # system status @staticmethod diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 6f5b52f14..f362e60ac 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -608,6 +608,12 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): def __str__(self): return '{0.name}({0.username})'.format(self) + @classmethod + def get_group_ids_by_user_id(cls, user_id): + group_ids = cls.groups.through.objects.filter(user_id=user_id).distinct().values_list('usergroup_id', flat=True) + group_ids = list(group_ids) + return group_ids + @property def is_wecom_bound(self): return bool(self.wecom_id) From a809eac2b8eea9b703a5fa6a34c9bb91dc41d2a4 Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 2 Jun 2021 17:36:47 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=20Metadata=20=E6=97=B6=EF=BC=8C=E8=8E=B7=E5=8F=96=E7=9A=84?= =?UTF-8?q?=E6=80=BB=E6=98=AF=20action=20=E4=B8=BA=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/drf/metadata.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py index cc2903d2f..3a0e98c50 100644 --- a/apps/common/drf/metadata.py +++ b/apps/common/drf/metadata.py @@ -18,7 +18,7 @@ from rest_framework.request import clone_request class SimpleMetadataWithFilters(SimpleMetadata): """Override SimpleMetadata, adding info about filters""" - methods = {"PUT", "POST", "GET"} + methods = {"PUT", "POST", "GET", "PATCH"} attrs = [ 'read_only', 'label', 'help_text', 'min_length', 'max_length', @@ -32,6 +32,7 @@ class SimpleMetadataWithFilters(SimpleMetadata): """ actions = {} for method in self.methods & set(view.allowed_methods): + view.action = view.action_map.get(method.lower(), view.action) view.request = clone_request(request, method) try: # Test global permissions From a9bdbcf7c6415ce8ff63c47da7b8a37f002d2bd6 Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 2 Jun 2021 19:11:08 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20metadata=20api=20view=20=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/drf/metadata.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py index 3a0e98c50..afd7389bb 100644 --- a/apps/common/drf/metadata.py +++ b/apps/common/drf/metadata.py @@ -32,7 +32,9 @@ class SimpleMetadataWithFilters(SimpleMetadata): """ actions = {} for method in self.methods & set(view.allowed_methods): - view.action = view.action_map.get(method.lower(), view.action) + if hasattr(view, 'action_map'): + view.action = view.action_map.get(method.lower(), view.action) + view.request = clone_request(request, method) try: # Test global permissions