mirror of https://github.com/jumpserver/jumpserver
feat: 站内信 (#6183)
* 添加站内信 * s * s * 添加接口 * fix * fix * 重构了一些 * 完成 * 完善 * s * s * s * s * s * s * 测试ok * 替换业务中发送消息的方式 * 修改 * s * 去掉 update 兼容 create * 添加 unread total 接口 * 调整json字段 Co-authored-by: xinwen <coderWen@126.com>pull/6214/head
parent
b82e9f860b
commit
4ef3b2630a
|
@ -15,6 +15,7 @@ dump.rdb
|
||||||
.tox
|
.tox
|
||||||
.cache/
|
.cache/
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
config.py
|
config.py
|
||||||
config.yml
|
config.yml
|
||||||
|
|
|
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
||||||
'applications.apps.ApplicationsConfig',
|
'applications.apps.ApplicationsConfig',
|
||||||
'tickets.apps.TicketsConfig',
|
'tickets.apps.TicketsConfig',
|
||||||
'acls.apps.AclsConfig',
|
'acls.apps.AclsConfig',
|
||||||
|
'notifications',
|
||||||
'common.apps.CommonConfig',
|
'common.apps.CommonConfig',
|
||||||
'jms_oidc_rp',
|
'jms_oidc_rp',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
|
|
@ -23,6 +23,7 @@ api_v1 = [
|
||||||
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
||||||
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
||||||
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
|
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()),
|
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .notifications import *
|
||||||
|
from .site_msgs import *
|
|
@ -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)
|
|
@ -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'})
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationsConfig(AppConfig):
|
||||||
|
name = 'notifications'
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .notification import *
|
||||||
|
from .site_msg import *
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .notifications import *
|
||||||
|
from .site_msgs import *
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'))
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -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
|
|
@ -13,4 +13,5 @@ class OpsConfig(AppConfig):
|
||||||
from orgs.utils import set_current_org
|
from orgs.utils import set_current_org
|
||||||
set_current_org(Organization.root())
|
set_current_org(Organization.root())
|
||||||
from .celery import signal_handler
|
from .celery import signal_handler
|
||||||
|
from . import notifications
|
||||||
super().ready()
|
super().ready()
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import ugettext
|
||||||
from django.db import models
|
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 common.utils import lazyproperty
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
@ -99,12 +99,12 @@ class CommandExecution(OrgModelMixin):
|
||||||
else:
|
else:
|
||||||
msg = _("Command `{}` is forbidden ........").format(self.command)
|
msg = _("Command `{}` is forbidden ........").format(self.command)
|
||||||
print('\033[31m' + msg + '\033[0m')
|
print('\033[31m' + msg + '\033[0m')
|
||||||
send_command_execution_alert_mail({
|
CommandExecutionAlert({
|
||||||
'input': self.command,
|
'input': self.command,
|
||||||
'assets': self.hosts.all(),
|
'assets': self.hosts.all(),
|
||||||
'user': str(self.user),
|
'user': str(self.user),
|
||||||
'risk_level': 5,
|
'risk_level': 5,
|
||||||
})
|
}).publish_async()
|
||||||
self.result = {"error": msg}
|
self.result = {"error": msg}
|
||||||
self.org_id = self.run_as.org_id
|
self.org_id = self.run_as.org_id
|
||||||
self.is_finished = True
|
self.is_finished = True
|
||||||
|
|
|
@ -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)
|
|
@ -20,7 +20,7 @@ from .celery.utils import (
|
||||||
disable_celery_periodic_task, delete_celery_periodic_task
|
disable_celery_periodic_task, delete_celery_periodic_task
|
||||||
)
|
)
|
||||||
from .models import Task, CommandExecution, CeleryTask
|
from .models import Task, CommandExecution, CeleryTask
|
||||||
from .utils import send_server_performance_mail
|
from .notifications import ServerPerformanceMessage
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ def check_server_performance_period():
|
||||||
if path.startswith(uncheck_path):
|
if path.startswith(uncheck_path):
|
||||||
need_check = False
|
need_check = False
|
||||||
if need_check and usage.percent > 80:
|
if need_check and usage.percent > 80:
|
||||||
send_server_performance_mail(path, usage, usages)
|
ServerPerformanceMessage(path=path, usage=usage).publish()
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="ansible")
|
@shared_task(queue="ansible")
|
||||||
|
|
|
@ -69,16 +69,6 @@ def update_or_create_ansible_task(
|
||||||
return task, created
|
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):
|
def get_task_log_path(base_path, task_id, level=2):
|
||||||
task_id = str(task_id)
|
task_id = str(task_id)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -4,28 +4,24 @@ import time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.shortcuts import HttpResponse
|
from django.shortcuts import HttpResponse
|
||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import action
|
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
|
|
||||||
from common.http import is_true
|
from terminal.models import CommandStorage
|
||||||
from terminal.models import CommandStorage, Command
|
|
||||||
from terminal.filters import CommandFilter
|
from terminal.filters import CommandFilter
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
|
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
|
||||||
from common.const.http import GET
|
|
||||||
from common.drf.api import JMSBulkModelViewSet
|
from common.drf.api import JMSBulkModelViewSet
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from terminal.utils import send_command_alert_mail
|
|
||||||
from terminal.serializers import InsecureCommandAlertSerializer
|
from terminal.serializers import InsecureCommandAlertSerializer
|
||||||
from terminal.exceptions import StorageInvalid
|
from terminal.exceptions import StorageInvalid
|
||||||
from ..backends import (
|
from ..backends import (
|
||||||
get_command_storage, get_multi_command_storage,
|
get_command_storage, get_multi_command_storage,
|
||||||
SessionCommandSerializer,
|
SessionCommandSerializer,
|
||||||
)
|
)
|
||||||
|
from ..notifications import CommandAlertMessage
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
__all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI']
|
__all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI']
|
||||||
|
@ -211,5 +207,5 @@ class InsecureCommandAlertAPI(generics.CreateAPIView):
|
||||||
if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL and \
|
if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL and \
|
||||||
settings.SECURITY_INSECURE_COMMAND and \
|
settings.SECURITY_INSECURE_COMMAND and \
|
||||||
settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER:
|
settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER:
|
||||||
send_command_alert_mail(command)
|
CommandAlertMessage(command).publish_async()
|
||||||
return Response()
|
return Response()
|
||||||
|
|
|
@ -10,4 +10,5 @@ class TerminalConfig(AppConfig):
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signals_handler
|
from . import signals_handler
|
||||||
|
from . import notifications
|
||||||
return super().ready()
|
return super().ready()
|
||||||
|
|
|
@ -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
|
||||||
|
<br>
|
||||||
|
Asset: %(host_name)s (%(host_ip)s)
|
||||||
|
<br>
|
||||||
|
User: %(user)s
|
||||||
|
<br>
|
||||||
|
Level: %(risk_level)s
|
||||||
|
<br>
|
||||||
|
Session: <a href="%(session_detail_url)s">session detail</a>
|
||||||
|
<br>
|
||||||
|
""") % {
|
||||||
|
'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', '<br>')
|
||||||
|
|
||||||
|
assets = ', '.join([str(asset) for asset in command['assets']])
|
||||||
|
message = _("""
|
||||||
|
<br>
|
||||||
|
Assets: %(assets)s
|
||||||
|
<br>
|
||||||
|
User: %(user)s
|
||||||
|
<br>
|
||||||
|
Level: %(risk_level)s
|
||||||
|
<br>
|
||||||
|
|
||||||
|
----------------- Commands ---------------- <br>
|
||||||
|
%(command)s <br>
|
||||||
|
----------------- Commands ---------------- <br>
|
||||||
|
""") % {
|
||||||
|
'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
|
||||||
|
}
|
|
@ -68,78 +68,6 @@ def get_session_replay_url(session):
|
||||||
return local_path, url
|
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
|
|
||||||
<br>
|
|
||||||
Asset: %(host_name)s (%(host_ip)s)
|
|
||||||
<br>
|
|
||||||
User: %(user)s
|
|
||||||
<br>
|
|
||||||
Level: %(risk_level)s
|
|
||||||
<br>
|
|
||||||
Session: <a href="%(session_detail_url)s">session detail</a>
|
|
||||||
<br>
|
|
||||||
""") % {
|
|
||||||
'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', '<br>')
|
|
||||||
recipient_list = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',')
|
|
||||||
|
|
||||||
assets = ', '.join([str(asset) for asset in command['assets']])
|
|
||||||
message = _("""
|
|
||||||
<br>
|
|
||||||
Assets: %(assets)s
|
|
||||||
<br>
|
|
||||||
User: %(user)s
|
|
||||||
<br>
|
|
||||||
Level: %(risk_level)s
|
|
||||||
<br>
|
|
||||||
|
|
||||||
----------------- Commands ---------------- <br>
|
|
||||||
%(command)s <br>
|
|
||||||
----------------- Commands ---------------- <br>
|
|
||||||
""") % {
|
|
||||||
'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:
|
class ComputeStatUtil:
|
||||||
# system status
|
# system status
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -608,6 +608,12 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{0.name}({0.username})'.format(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
|
@property
|
||||||
def is_wecom_bound(self):
|
def is_wecom_bound(self):
|
||||||
return bool(self.wecom_id)
|
return bool(self.wecom_id)
|
||||||
|
|
Loading…
Reference in New Issue