Merge pull request #6265 from jumpserver/dev

v2.11.0 rc2
This commit is contained in:
Jiangjie.Bai 2021-06-15 10:49:46 +08:00 committed by GitHub
commit 84070a558e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 424 additions and 180 deletions

View File

@ -1,3 +1,4 @@
from .application import * from .application import *
from .application_user import *
from .mixin import * from .mixin import *
from .remote_app import * from .remote_app import *

View File

@ -2,18 +2,13 @@
# #
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from rest_framework import generics
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin from ..hands import IsOrgAdminOrAppUser
from .. import models, serializers from .. import serializers
from ..models import Application from ..models import Application
from assets.models import SystemUser
from assets.serializers import SystemUserListSerializer
from perms.models import ApplicationPermission
from ..const import ApplicationCategoryChoices
__all__ = ['ApplicationViewSet', 'ApplicationUserListApi'] __all__ = ['ApplicationViewSet']
class ApplicationViewSet(OrgBulkModelViewSet): class ApplicationViewSet(OrgBulkModelViewSet):
@ -22,29 +17,3 @@ class ApplicationViewSet(OrgBulkModelViewSet):
search_fields = filterset_fields search_fields = filterset_fields
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.ApplicationSerializer serializer_class = serializers.ApplicationSerializer
class ApplicationUserListApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin, )
filterset_fields = ('name', 'username')
search_fields = filterset_fields
serializer_class = SystemUserListSerializer
def get_application(self):
application = None
app_id = self.request.query_params.get('application_id')
if app_id:
application = Application.objects.get(id=app_id)
return application
def get_queryset(self):
queryset = SystemUser.objects.none()
application = self.get_application()
if not application:
return queryset
system_user_ids = ApplicationPermission.objects.filter(applications=application)\
.values_list('system_users', flat=True)
if not system_user_ids:
return queryset
queryset = SystemUser.objects.filter(id__in=system_user_ids)
return queryset

View File

@ -0,0 +1,55 @@
# coding: utf-8
#
from rest_framework import generics
from django.conf import settings
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
from .. import serializers
from ..models import Application, ApplicationUser
from perms.models import ApplicationPermission
class ApplicationUserListApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin, )
filterset_fields = ('name', 'username')
search_fields = filterset_fields
serializer_class = serializers.ApplicationUserSerializer
_application = None
@property
def application(self):
if self._application is None:
app_id = self.request.query_params.get('application_id')
if app_id:
self._application = Application.objects.get(id=app_id)
return self._application
def get_serializer_context(self):
context = super().get_serializer_context()
context.update({
'application': self.application
})
return context
def get_queryset(self):
queryset = ApplicationUser.objects.none()
if not self.application:
return queryset
system_user_ids = ApplicationPermission.objects.filter(applications=self.application)\
.values_list('system_users', flat=True)
if not system_user_ids:
return queryset
queryset = ApplicationUser.objects.filter(id__in=system_user_ids)
return queryset
class ApplicationUserAuthInfoListApi(ApplicationUserListApi):
serializer_class = serializers.ApplicationUserWithAuthInfoSerializer
http_method_names = ['get']
permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self):
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
return super().get_permissions()

View File

@ -11,5 +11,5 @@
""" """
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
from users.models import User, UserGroup from users.models import User, UserGroup

View File

@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin from common.mixins import CommonModelMixin
from assets.models import Asset from assets.models import Asset, SystemUser
from .. import const from .. import const
@ -68,3 +68,8 @@ class Application(CommonModelMixin, OrgModelMixin):
raise ValueError("Remote App not has asset attr") raise ValueError("Remote App not has asset attr")
asset = Asset.objects.filter(id=asset_id).first() asset = Asset.objects.filter(id=asset_id).first()
return asset return asset
class ApplicationUser(SystemUser):
class Meta:
proxy = True

View File

@ -6,11 +6,12 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import MethodSerializer from common.drf.serializers import MethodSerializer
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
from assets.serializers import SystemUserSerializer
from .. import models from .. import models
__all__ = [ __all__ = [
'ApplicationSerializer', 'ApplicationSerializerMixin', 'ApplicationSerializer', 'ApplicationSerializerMixin',
'ApplicationUserSerializer', 'ApplicationUserWithAuthInfoSerializer'
] ]
@ -66,3 +67,42 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
_attrs.update(attrs) _attrs.update(attrs)
return _attrs return _attrs
class ApplicationUserSerializer(SystemUserSerializer):
application_name = serializers.SerializerMethodField(label=_('Application name'))
application_category = serializers.SerializerMethodField(label=_('Application category'))
application_type = serializers.SerializerMethodField(label=_('Application type'))
class Meta(SystemUserSerializer.Meta):
model = models.ApplicationUser
fields_mini = [
'id', 'application_name', 'application_category', 'application_type', 'name', 'username'
]
fields_small = fields_mini + [
'protocol', 'login_mode', 'login_mode_display', 'priority',
"username_same_with_user", 'comment',
]
fields = fields_small
extra_kwargs = {
'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True},
}
@property
def application(self):
return self.context['application']
def get_application_name(self, obj):
return self.application.name
def get_application_category(self, obj):
return self.application.get_category_display()
def get_application_type(self, obj):
return self.application.get_type_display()
class ApplicationUserWithAuthInfoSerializer(ApplicationUserSerializer):
class Meta(ApplicationUserSerializer.Meta):
fields = ApplicationUserSerializer.Meta.fields + ['password']

View File

@ -14,7 +14,8 @@ router.register(r'applications', api.ApplicationViewSet, 'application')
urlpatterns = [ urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
path('application-users/', api.ApplicationUserListApi.as_view(), name='application-user') path('application-users/', api.ApplicationUserListApi.as_view(), name='application-user'),
path('application-user-auth-infos/', api.ApplicationUserAuthInfoListApi.as_view(), name='application-user-auth-info')
] ]

View File

@ -94,8 +94,14 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
date_range_filter_fields = [ date_range_filter_fields = [
('date_start', ('date_from', 'date_to')) ('date_start', ('date_from', 'date_to'))
] ]
filterset_fields = ['user__name', 'command', 'run_as__name', 'is_finished'] filterset_fields = [
search_fields = ['command', 'user__name', 'run_as__name'] 'user__name', 'user__username', 'command',
'run_as__name', 'run_as__username', 'is_finished'
]
search_fields = [
'command', 'user__name', 'user__username',
'run_as__name', 'run_as__username',
]
ordering = ['-date_created'] ordering = ['-date_created']
def get_queryset(self): def get_queryset(self):

View File

@ -82,7 +82,7 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
model = CommandExecution model = CommandExecution
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'run_as', 'command', 'user', 'is_finished', 'run_as', 'command', 'is_finished', 'user',
'date_start', 'result', 'is_success', 'org_id' 'date_start', 'result', 'is_success', 'org_id'
] ]
fields = fields_small + ['hosts', 'hosts_display', 'run_as_display', 'user_display'] fields = fields_small + ['hosts', 'hosts_display', 'run_as_display', 'user_display']

View File

@ -57,6 +57,7 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key') backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password') backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO') backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom') backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk') backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
return backend_label_mapping return backend_label_mapping
@ -156,12 +157,13 @@ def get_login_backend(request):
return backend_label return backend_label
def generate_data(username, request): def generate_data(username, request, login_type=None):
user_agent = request.META.get('HTTP_USER_AGENT', '') user_agent = request.META.get('HTTP_USER_AGENT', '')
login_ip = get_request_ip(request) or '0.0.0.0' login_ip = get_request_ip(request) or '0.0.0.0'
if isinstance(request, Request):
if login_type is None and isinstance(request, Request):
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U') login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U')
else: if login_type is None:
login_type = 'W' login_type = 'W'
data = { data = {
@ -176,9 +178,9 @@ def generate_data(username, request):
@receiver(post_auth_success) @receiver(post_auth_success)
def on_user_auth_success(sender, user, request, **kwargs): def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
logger.debug('User login success: {}'.format(user.username)) logger.debug('User login success: {}'.format(user.username))
data = generate_data(user.username, request) data = generate_data(user.username, request, login_type=login_type)
data.update({'mfa': int(user.mfa_enabled), 'status': True}) data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log(**data) write_login_log(**data)

View File

@ -6,12 +6,14 @@ from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.translation import ugettext as _
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework import serializers from rest_framework import serializers
from authentication.signals import post_auth_failed, post_auth_success
from common.utils import get_logger, random_string from common.utils import get_logger, random_string
from common.drf.api import SerializerMixin2 from common.drf.api import SerializerMixin2
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
@ -51,10 +53,6 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
return True return True
def create_token(self, user, asset, application, system_user, ttl=5*60): def create_token(self, user, asset, application, system_user, ttl=5*60):
if not settings.CONNECTION_TOKEN_ENABLED:
raise PermissionDenied('Connection token disabled')
if not user:
user = self.request.user
if not self.request.user.is_superuser and user != self.request.user: if not self.request.user.is_superuser and user != self.request.user:
raise PermissionDenied('Only super user can create user token') raise PermissionDenied('Only super user can create user token')
self.check_resource_permission(user, asset, application, system_user) self.check_resource_permission(user, asset, application, system_user)
@ -233,12 +231,24 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
if asset and not asset.is_active: if asset and not asset.is_active:
raise serializers.ValidationError("Asset disabled") raise serializers.ValidationError("Asset disabled")
try:
self.check_resource_permission(user, asset, app, system_user)
except PermissionDenied:
raise serializers.ValidationError('Permission expired or invalid')
return value, user, system_user, asset, app return value, user, system_user, asset, app
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail') @action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs): def get_secret_detail(self, request, *args, **kwargs):
token = request.data.get('token', '') token = request.data.get('token', '')
value, user, system_user, asset, app = self.valid_token(token) try:
value, user, system_user, asset, app = self.valid_token(token)
except serializers.ValidationError as e:
post_auth_failed.send(
sender=self.__class__, username='', request=self.request,
reason=_('Invalid token')
)
raise e
data = dict(user=user, system_user=system_user) data = dict(user=user, system_user=system_user)
if asset: if asset:
@ -252,6 +262,9 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
data['type'] = 'application' data['type'] = 'application'
data.update(app_detail) data.update(app_detail)
self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN
post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T')
serializer = self.get_serializer(data) serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200) return Response(data=serializer.data, status=200)

View File

@ -228,3 +228,11 @@ class DingTalkAuthentication(ModelBackend):
def authenticate(self, request, **kwargs): def authenticate(self, request, **kwargs):
pass pass
class AuthorizationTokenAuthentication(ModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass

View File

@ -2,9 +2,11 @@ from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from ops.urls.ws_urls import urlpatterns as ops_urlpatterns from ops.urls.ws_urls import urlpatterns as ops_urlpatterns
from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns
urlpatterns = [] urlpatterns = []
urlpatterns += ops_urlpatterns urlpatterns += ops_urlpatterns \
+ notifications_urlpatterns
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack( 'websocket': AuthMiddlewareStack(

View File

@ -130,10 +130,12 @@ AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend'
AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication' AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication'
AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication' AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication'
AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication' AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthentication'
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM,
AUTH_BACKEND_DINGTALK, AUTH_BACKEND_AUTH_TOKEN
] ]
if AUTH_CAS: if AUTH_CAS:

View File

@ -48,7 +48,7 @@ INSTALLED_APPS = [
'applications.apps.ApplicationsConfig', 'applications.apps.ApplicationsConfig',
'tickets.apps.TicketsConfig', 'tickets.apps.TicketsConfig',
'acls.apps.AclsConfig', 'acls.apps.AclsConfig',
'notifications', 'notifications.apps.NotificationsConfig',
'common.apps.CommonConfig', 'common.apps.CommonConfig',
'jms_oidc_rp', 'jms_oidc_rp',
'rest_framework', 'rest_framework',

View File

@ -23,7 +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.notifications', namespace='api-notifications')), path('notifications/', include('notifications.urls.api_urls', namespace='api-notifications')),
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()), path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
] ]

Binary file not shown.

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-06-04 11:29+0800\n" "POT-Creation-Date: 2021-06-11 11:06+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -121,7 +121,7 @@ msgstr "系统用户"
#: applications/serializers/attrs/application_category/remote_app.py:33 #: applications/serializers/attrs/application_category/remote_app.py:33
#: assets/models/asset.py:355 assets/models/authbook.py:26 #: assets/models/asset.py:355 assets/models/authbook.py:26
#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:34 #: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:34
#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:90 #: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:91
#: assets/serializers/system_user.py:202 audits/models.py:38 #: assets/serializers/system_user.py:202 audits/models.py:38
#: perms/models/asset_permission.py:99 templates/index.html:82 #: perms/models/asset_permission.py:99 templates/index.html:82
#: terminal/backends/command/models.py:19 #: terminal/backends/command/models.py:19
@ -158,7 +158,7 @@ msgstr ""
#: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31 #: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31
#: applications/serializers/attrs/application_type/mysql_workbench.py:18 #: applications/serializers/attrs/application_type/mysql_workbench.py:18
#: assets/models/asset.py:183 assets/models/domain.py:52 #: assets/models/asset.py:183 assets/models/domain.py:52
#: assets/serializers/asset_user.py:47 settings/serializers/settings.py:117 #: assets/serializers/asset_user.py:47 settings/serializers/settings.py:113
#: users/templates/users/_granted_assets.html:26 #: users/templates/users/_granted_assets.html:26
#: users/templates/users/user_asset_permission.html:156 #: users/templates/users/user_asset_permission.html:156
msgid "IP" msgid "IP"
@ -199,7 +199,7 @@ msgstr ""
#: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184 #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184
#: assets/serializers/asset_user.py:46 assets/serializers/gathered_user.py:23 #: assets/serializers/asset_user.py:46 assets/serializers/gathered_user.py:23
#: settings/serializers/settings.py:116 #: settings/serializers/settings.py:112
#: users/templates/users/_granted_assets.html:25 #: users/templates/users/_granted_assets.html:25
#: users/templates/users/user_asset_permission.html:157 #: users/templates/users/user_asset_permission.html:157
msgid "Hostname" msgid "Hostname"
@ -272,14 +272,32 @@ msgstr "网域"
msgid "Attrs" msgid "Attrs"
msgstr "" msgstr ""
#: applications/serializers/application.py:47 #: applications/serializers/application.py:48
msgid "Category(Display)" msgid "Category(Display)"
msgstr "类别 (显示名称)" msgstr "类别 (显示名称)"
#: applications/serializers/application.py:48 #: applications/serializers/application.py:49
msgid "Type(Dispaly)" msgid "Type(Dispaly)"
msgstr "类型 (显示名称)" msgstr "类型 (显示名称)"
#: applications/serializers/application.py:72
msgid "Application name"
msgstr "应用名称"
#: applications/serializers/application.py:73
msgid "Application category"
msgstr "应用类别"
#: applications/serializers/application.py:74
msgid "Application type"
msgstr "应用类型"
#: applications/serializers/application.py:87
#: assets/serializers/system_user.py:49 assets/serializers/system_user.py:177
#: assets/serializers/system_user.py:203
msgid "Login mode display"
msgstr "登录模式(显示名称)"
#: applications/serializers/attrs/application_category/cloud.py:9 #: applications/serializers/attrs/application_category/cloud.py:9
#: assets/models/cluster.py:40 #: assets/models/cluster.py:40
msgid "Cluster" msgid "Cluster"
@ -316,10 +334,10 @@ msgstr "目标URL"
#: applications/serializers/attrs/application_type/custom.py:25 #: applications/serializers/attrs/application_type/custom.py:25
#: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/mysql_workbench.py:34
#: applications/serializers/attrs/application_type/vmware_client.py:30 #: applications/serializers/attrs/application_type/vmware_client.py:30
#: assets/models/base.py:251 assets/serializers/asset_user.py:77 #: assets/models/base.py:251 assets/serializers/asset_user.py:78
#: audits/signals_handler.py:58 authentication/forms.py:22 #: audits/signals_handler.py:58 authentication/forms.py:22
#: authentication/templates/authentication/login.html:164 #: authentication/templates/authentication/login.html:164
#: settings/serializers/settings.py:98 users/forms/profile.py:21 #: settings/serializers/settings.py:94 users/forms/profile.py:21
#: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_otp_check_password.html:13
#: users/templates/users/user_password_update.html:43 #: users/templates/users/user_password_update.html:43
#: users/templates/users/user_password_verify.html:18 #: users/templates/users/user_password_verify.html:18
@ -822,12 +840,12 @@ msgstr "后端"
msgid "Source" msgid "Source"
msgstr "来源" msgstr "来源"
#: assets/serializers/asset_user.py:81 users/forms/profile.py:160 #: assets/serializers/asset_user.py:82 users/forms/profile.py:160
#: users/models/user.py:580 users/templates/users/user_password_update.html:48 #: users/models/user.py:580 users/templates/users/user_password_update.html:48
msgid "Public key" msgid "Public key"
msgstr "SSH公钥" msgstr "SSH公钥"
#: assets/serializers/asset_user.py:85 users/models/user.py:577 #: assets/serializers/asset_user.py:86 users/models/user.py:577
msgid "Private key" msgid "Private key"
msgstr "ssh私钥" msgstr "ssh私钥"
@ -875,11 +893,6 @@ msgstr "同级别节点名字不能重复"
msgid "Nodes amount" msgid "Nodes amount"
msgstr "节点数量" msgstr "节点数量"
#: assets/serializers/system_user.py:49 assets/serializers/system_user.py:177
#: assets/serializers/system_user.py:203
msgid "Login mode display"
msgstr "登录模式(显示名称)"
#: assets/serializers/system_user.py:51 assets/serializers/system_user.py:179 #: assets/serializers/system_user.py:51 assets/serializers/system_user.py:179
msgid "Ad domain" msgid "Ad domain"
msgstr "Ad 网域" msgstr "Ad 网域"
@ -2049,7 +2062,7 @@ msgstr "应用程序"
msgid "Application permission" msgid "Application permission"
msgstr "应用管理" msgstr "应用管理"
#: perms/models/asset_permission.py:37 settings/serializers/settings.py:121 #: perms/models/asset_permission.py:37 settings/serializers/settings.py:117
msgid "All" msgid "All"
msgstr "全部" msgstr "全部"
@ -2194,27 +2207,19 @@ msgstr "当前站点URL"
msgid "eg: http://dev.jumpserver.org:8080" msgid "eg: http://dev.jumpserver.org:8080"
msgstr "如: http://dev.jumpserver.org:8080" msgstr "如: http://dev.jumpserver.org:8080"
#: settings/serializers/settings.py:19 #: settings/serializers/settings.py:20
msgid "RDP address"
msgstr "RDP 地址"
#: settings/serializers/settings.py:21
msgid "RDP visit address, eg: dev.jumpserver.org:3389"
msgstr "RDP 访问地址, 如: dev.jumpserver.org:3389"
#: settings/serializers/settings.py:24
msgid "User guide url" msgid "User guide url"
msgstr "用户向导URL" msgstr "用户向导URL"
#: settings/serializers/settings.py:25 #: settings/serializers/settings.py:21
msgid "User first login update profile done redirect to it" msgid "User first login update profile done redirect to it"
msgstr "用户第一次登录修改profile后重定向到地址, 可以是 wiki 或 其他说明文档" msgstr "用户第一次登录修改profile后重定向到地址, 可以是 wiki 或 其他说明文档"
#: settings/serializers/settings.py:28 #: settings/serializers/settings.py:24
msgid "Forgot password url" msgid "Forgot password url"
msgstr "忘记密码URL" msgstr "忘记密码URL"
#: settings/serializers/settings.py:29 #: settings/serializers/settings.py:25
msgid "" msgid ""
"The forgot password url on login page, If you use ldap or cas external " "The forgot password url on login page, If you use ldap or cas external "
"authentication, you can set it" "authentication, you can set it"
@ -2222,138 +2227,138 @@ msgstr ""
"登录页面忘记密码URL, 如果使用了 LDAP, OPENID 等外部认证系统,可以自定义用户重" "登录页面忘记密码URL, 如果使用了 LDAP, OPENID 等外部认证系统,可以自定义用户重"
"置密码访问的地址" "置密码访问的地址"
#: settings/serializers/settings.py:33 #: settings/serializers/settings.py:29
msgid "Global organization name" msgid "Global organization name"
msgstr "全局组织名" msgstr "全局组织名"
#: settings/serializers/settings.py:34 #: settings/serializers/settings.py:30
msgid "The name of global organization to display" msgid "The name of global organization to display"
msgstr "全局组织的显示名称,默认为 全局组织" msgstr "全局组织的显示名称,默认为 全局组织"
#: settings/serializers/settings.py:41 #: settings/serializers/settings.py:37
msgid "SMTP host" msgid "SMTP host"
msgstr "SMTP 主机" msgstr "SMTP 主机"
#: settings/serializers/settings.py:42 #: settings/serializers/settings.py:38
msgid "SMTP port" msgid "SMTP port"
msgstr "SMTP 端口" msgstr "SMTP 端口"
#: settings/serializers/settings.py:43 #: settings/serializers/settings.py:39
msgid "SMTP account" msgid "SMTP account"
msgstr "SMTP 账号" msgstr "SMTP 账号"
#: settings/serializers/settings.py:45 #: settings/serializers/settings.py:41
msgid "SMTP password" msgid "SMTP password"
msgstr "SMTP 密码" msgstr "SMTP 密码"
#: settings/serializers/settings.py:46 #: settings/serializers/settings.py:42
msgid "Tips: Some provider use token except password" msgid "Tips: Some provider use token except password"
msgstr "提示:一些邮件提供商需要输入的是授权码" msgstr "提示:一些邮件提供商需要输入的是授权码"
#: settings/serializers/settings.py:49 #: settings/serializers/settings.py:45
msgid "Send user" msgid "Send user"
msgstr "发件人" msgstr "发件人"
#: settings/serializers/settings.py:50 #: settings/serializers/settings.py:46
msgid "Tips: Send mail account, default SMTP account as the send account" msgid "Tips: Send mail account, default SMTP account as the send account"
msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号" msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号"
#: settings/serializers/settings.py:53 #: settings/serializers/settings.py:49
msgid "Test recipient" msgid "Test recipient"
msgstr "测试收件人" msgstr "测试收件人"
#: settings/serializers/settings.py:54 #: settings/serializers/settings.py:50
msgid "Tips: Used only as a test mail recipient" msgid "Tips: Used only as a test mail recipient"
msgstr "提示:仅用来作为测试邮件收件人" msgstr "提示:仅用来作为测试邮件收件人"
#: settings/serializers/settings.py:57 #: settings/serializers/settings.py:53
msgid "Use SSL" msgid "Use SSL"
msgstr "使用 SSL" msgstr "使用 SSL"
#: settings/serializers/settings.py:58 #: settings/serializers/settings.py:54
msgid "If SMTP port is 465, may be select" msgid "If SMTP port is 465, may be select"
msgstr "如果SMTP端口是465通常需要启用 SSL" msgstr "如果SMTP端口是465通常需要启用 SSL"
#: settings/serializers/settings.py:61 #: settings/serializers/settings.py:57
msgid "Use TLS" msgid "Use TLS"
msgstr "使用 TLS" msgstr "使用 TLS"
#: settings/serializers/settings.py:62 #: settings/serializers/settings.py:58
msgid "If SMTP port is 587, may be select" msgid "If SMTP port is 587, may be select"
msgstr "如果SMTP端口是587通常需要启用 TLS" msgstr "如果SMTP端口是587通常需要启用 TLS"
#: settings/serializers/settings.py:65 #: settings/serializers/settings.py:61
msgid "Subject prefix" msgid "Subject prefix"
msgstr "主题前缀" msgstr "主题前缀"
#: settings/serializers/settings.py:72 #: settings/serializers/settings.py:68
msgid "Create user email subject" msgid "Create user email subject"
msgstr "邮件主题" msgstr "邮件主题"
#: settings/serializers/settings.py:73 #: settings/serializers/settings.py:69
msgid "" msgid ""
"Tips: When creating a user, send the subject of the email (eg:Create account " "Tips: When creating a user, send the subject of the email (eg:Create account "
"successfully)" "successfully)"
msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)"
#: settings/serializers/settings.py:77 #: settings/serializers/settings.py:73
msgid "Create user honorific" msgid "Create user honorific"
msgstr "邮件的敬语" msgstr "邮件的敬语"
#: settings/serializers/settings.py:78 #: settings/serializers/settings.py:74
msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)"
msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)" msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)"
#: settings/serializers/settings.py:82 #: settings/serializers/settings.py:78
msgid "Create user email content" msgid "Create user email content"
msgstr "邮件的内容" msgstr "邮件的内容"
#: settings/serializers/settings.py:83 #: settings/serializers/settings.py:79
msgid "Tips:When creating a user, send the content of the email" msgid "Tips:When creating a user, send the content of the email"
msgstr "提示: 创建用户时,发送设置密码邮件的内容" msgstr "提示: 创建用户时,发送设置密码邮件的内容"
#: settings/serializers/settings.py:86 #: settings/serializers/settings.py:82
msgid "Signature" msgid "Signature"
msgstr "署名" msgstr "署名"
#: settings/serializers/settings.py:87 #: settings/serializers/settings.py:83
msgid "Tips: Email signature (eg:jumpserver)" msgid "Tips: Email signature (eg:jumpserver)"
msgstr "邮件署名 (如:jumpserver)" msgstr "邮件署名 (如:jumpserver)"
#: settings/serializers/settings.py:95 #: settings/serializers/settings.py:91
msgid "LDAP server" msgid "LDAP server"
msgstr "LDAP 地址" msgstr "LDAP 地址"
#: settings/serializers/settings.py:95 #: settings/serializers/settings.py:91
msgid "eg: ldap://localhost:389" msgid "eg: ldap://localhost:389"
msgstr "" msgstr ""
#: settings/serializers/settings.py:97 #: settings/serializers/settings.py:93
msgid "Bind DN" msgid "Bind DN"
msgstr "绑定 DN" msgstr "绑定 DN"
#: settings/serializers/settings.py:100 #: settings/serializers/settings.py:96
msgid "User OU" msgid "User OU"
msgstr "用户 OU" msgstr "用户 OU"
#: settings/serializers/settings.py:101 #: settings/serializers/settings.py:97
msgid "Use | split multi OUs" msgid "Use | split multi OUs"
msgstr "多个 OU 使用 | 分割" msgstr "多个 OU 使用 | 分割"
#: settings/serializers/settings.py:104 #: settings/serializers/settings.py:100
msgid "User search filter" msgid "User search filter"
msgstr "用户过滤器" msgstr "用户过滤器"
#: settings/serializers/settings.py:105 #: settings/serializers/settings.py:101
#, python-format #, python-format
msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)"
msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)"
#: settings/serializers/settings.py:108 #: settings/serializers/settings.py:104
msgid "User attr map" msgid "User attr map"
msgstr "用户属性映射" msgstr "用户属性映射"
#: settings/serializers/settings.py:109 #: settings/serializers/settings.py:105
msgid "" msgid ""
"User attr map present how to map LDAP user attr to jumpserver, username,name," "User attr map present how to map LDAP user attr to jumpserver, username,name,"
"email is jumpserver attr" "email is jumpserver attr"
@ -2361,23 +2366,23 @@ msgstr ""
"用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上username, name," "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上username, name,"
"email 是jumpserver的用户需要属性" "email 是jumpserver的用户需要属性"
#: settings/serializers/settings.py:111 #: settings/serializers/settings.py:107
msgid "Enable LDAP auth" msgid "Enable LDAP auth"
msgstr "启用 LDAP 认证" msgstr "启用 LDAP 认证"
#: settings/serializers/settings.py:122 #: settings/serializers/settings.py:118
msgid "Auto" msgid "Auto"
msgstr "自动" msgstr "自动"
#: settings/serializers/settings.py:128 #: settings/serializers/settings.py:124
msgid "Password auth" msgid "Password auth"
msgstr "密码认证" msgstr "密码认证"
#: settings/serializers/settings.py:130 #: settings/serializers/settings.py:126
msgid "Public key auth" msgid "Public key auth"
msgstr "密钥认证" msgstr "密钥认证"
#: settings/serializers/settings.py:131 #: settings/serializers/settings.py:127
msgid "" msgid ""
"Tips: If use other auth method, like AD/LDAP, you should disable this to " "Tips: If use other auth method, like AD/LDAP, you should disable this to "
"avoid being able to log in after deleting" "avoid being able to log in after deleting"
@ -2385,19 +2390,19 @@ msgstr ""
"提示:如果你使用其它认证方式,如 AD/LDAP你应该禁用此项以避免第三方系统删" "提示:如果你使用其它认证方式,如 AD/LDAP你应该禁用此项以避免第三方系统删"
"除后,还可以登录" "除后,还可以登录"
#: settings/serializers/settings.py:134 #: settings/serializers/settings.py:130
msgid "List sort by" msgid "List sort by"
msgstr "资产列表排序" msgstr "资产列表排序"
#: settings/serializers/settings.py:135 #: settings/serializers/settings.py:131
msgid "List page size" msgid "List page size"
msgstr "资产列表每页数量" msgstr "资产列表每页数量"
#: settings/serializers/settings.py:137 #: settings/serializers/settings.py:133
msgid "Session keep duration" msgid "Session keep duration"
msgstr "会话日志保存时间" msgstr "会话日志保存时间"
#: settings/serializers/settings.py:138 #: settings/serializers/settings.py:134
msgid "" msgid ""
"Units: days, Session, record, command will be delete if more than duration, " "Units: days, Session, record, command will be delete if more than duration, "
"only in database" "only in database"
@ -2405,64 +2410,72 @@ msgstr ""
"单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不"
"受影响)" "受影响)"
#: settings/serializers/settings.py:140 #: settings/serializers/settings.py:136
msgid "Telnet login regex" msgid "Telnet login regex"
msgstr "Telnet 成功正则表达式" msgstr "Telnet 成功正则表达式"
#: settings/serializers/settings.py:145 #: settings/serializers/settings.py:138
msgid "RDP address"
msgstr "RDP 地址"
#: settings/serializers/settings.py:141
msgid "RDP visit address, eg: dev.jumpserver.org:3389"
msgstr "RDP 访问地址, 如: dev.jumpserver.org:3389"
#: settings/serializers/settings.py:147
msgid "Global MFA auth" msgid "Global MFA auth"
msgstr "全局启用 MFA 认证" msgstr "全局启用 MFA 认证"
#: settings/serializers/settings.py:146 #: settings/serializers/settings.py:148
msgid "All user enable MFA" msgid "All user enable MFA"
msgstr "强制所有用户启用多因子认证" msgstr "强制所有用户启用多因子认证"
#: settings/serializers/settings.py:149 #: settings/serializers/settings.py:151
msgid "Batch command execution" msgid "Batch command execution"
msgstr "批量命令执行" msgstr "批量命令执行"
#: settings/serializers/settings.py:150 #: settings/serializers/settings.py:152
msgid "Allow user run batch command or not using ansible" msgid "Allow user run batch command or not using ansible"
msgstr "是否允许用户使用 ansible 执行批量命令" msgstr "是否允许用户使用 ansible 执行批量命令"
#: settings/serializers/settings.py:153 #: settings/serializers/settings.py:155
msgid "Enable terminal register" msgid "Enable terminal register"
msgstr "终端注册" msgstr "终端注册"
#: settings/serializers/settings.py:154 #: settings/serializers/settings.py:156
msgid "" msgid ""
"Allow terminal register, after all terminal setup, you should disable this " "Allow terminal register, after all terminal setup, you should disable this "
"for security" "for security"
msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭"
#: settings/serializers/settings.py:158 #: settings/serializers/settings.py:160
msgid "Limit the number of login failures" msgid "Limit the number of login failures"
msgstr "限制登录失败次数" msgstr "限制登录失败次数"
#: settings/serializers/settings.py:162 #: settings/serializers/settings.py:164
msgid "Block logon interval" msgid "Block logon interval"
msgstr "禁止登录时间间隔" msgstr "禁止登录时间间隔"
#: settings/serializers/settings.py:163 #: settings/serializers/settings.py:165
msgid "" msgid ""
"Tip: (unit/minute) if the user has failed to log in for a limited number of " "Tip: (unit/minute) if the user has failed to log in for a limited number of "
"times, no login is allowed during this time interval." "times, no login is allowed during this time interval."
msgstr "" msgstr ""
"提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录"
#: settings/serializers/settings.py:167 #: settings/serializers/settings.py:169
msgid "Connection max idle time" msgid "Connection max idle time"
msgstr "连接最大空闲时间" msgstr "连接最大空闲时间"
#: settings/serializers/settings.py:168 #: settings/serializers/settings.py:170
msgid "If idle time more than it, disconnect connection Unit: minute" msgid "If idle time more than it, disconnect connection Unit: minute"
msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)"
#: settings/serializers/settings.py:172 #: settings/serializers/settings.py:174
msgid "User password expiration" msgid "User password expiration"
msgstr "用户密码过期时间" msgstr "用户密码过期时间"
#: settings/serializers/settings.py:173 #: settings/serializers/settings.py:175
msgid "" msgid ""
"Tip: (unit: day) If the user does not update the password during the time, " "Tip: (unit: day) If the user does not update the password during the time, "
"the user password will expire failure;The password expiration reminder mail " "the user password will expire failure;The password expiration reminder mail "
@ -2472,53 +2485,53 @@ msgstr ""
"提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期"
"提醒邮件将在密码过期前5天内由系统每天自动发送给用户" "提醒邮件将在密码过期前5天内由系统每天自动发送给用户"
#: settings/serializers/settings.py:177 #: settings/serializers/settings.py:179
msgid "Number of repeated historical passwords" msgid "Number of repeated historical passwords"
msgstr "不能设置近几次密码" msgstr "不能设置近几次密码"
#: settings/serializers/settings.py:178 #: settings/serializers/settings.py:180
msgid "" msgid ""
"Tip: When the user resets the password, it cannot be the previous n " "Tip: When the user resets the password, it cannot be the previous n "
"historical passwords of the user" "historical passwords of the user"
msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码"
#: settings/serializers/settings.py:182 #: settings/serializers/settings.py:184
msgid "Password minimum length" msgid "Password minimum length"
msgstr "密码最小长度" msgstr "密码最小长度"
#: settings/serializers/settings.py:185 #: settings/serializers/settings.py:187
msgid "Must contain capital" msgid "Must contain capital"
msgstr "必须包含大写字符" msgstr "必须包含大写字符"
#: settings/serializers/settings.py:187 #: settings/serializers/settings.py:189
msgid "Must contain lowercase" msgid "Must contain lowercase"
msgstr "必须包含小写字符" msgstr "必须包含小写字符"
#: settings/serializers/settings.py:188 #: settings/serializers/settings.py:190
msgid "Must contain numeric" msgid "Must contain numeric"
msgstr "必须包含数字" msgstr "必须包含数字"
#: settings/serializers/settings.py:189 #: settings/serializers/settings.py:191
msgid "Must contain special" msgid "Must contain special"
msgstr "必须包含特殊字符" msgstr "必须包含特殊字符"
#: settings/serializers/settings.py:190 #: settings/serializers/settings.py:192
msgid "Insecure command alert" msgid "Insecure command alert"
msgstr "危险命令告警" msgstr "危险命令告警"
#: settings/serializers/settings.py:192 #: settings/serializers/settings.py:194
msgid "Email recipient" msgid "Email recipient"
msgstr "邮件收件人" msgstr "邮件收件人"
#: settings/serializers/settings.py:193 #: settings/serializers/settings.py:195
msgid "Multiple user using , split" msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割" msgstr "多个用户,使用 , 分割"
#: settings/serializers/settings.py:201 #: settings/serializers/settings.py:203
msgid "Enable WeCom Auth" msgid "Enable WeCom Auth"
msgstr "启用企业微信认证" msgstr "启用企业微信认证"
#: settings/serializers/settings.py:208 #: settings/serializers/settings.py:210
msgid "Enable DingTalk Auth" msgid "Enable DingTalk Auth"
msgstr "启用钉钉认证" msgstr "启用钉钉认证"

View File

@ -10,7 +10,7 @@ from ..serializers import (
SiteMessageDetailSerializer, SiteMessageIdsSerializer, SiteMessageDetailSerializer, SiteMessageIdsSerializer,
SiteMessageSendSerializer, SiteMessageSendSerializer,
) )
from ..site_msg import SiteMessage from ..site_msg import SiteMessageUtil
from ..filters import SiteMsgFilter from ..filters import SiteMsgFilter
__all__ = ('SiteMessageViewSet', ) __all__ = ('SiteMessageViewSet', )
@ -30,15 +30,15 @@ class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JmsGenericViewSet):
has_read = self.request.query_params.get('has_read') has_read = self.request.query_params.get('has_read')
if has_read is None: if has_read is None:
msgs = SiteMessage.get_user_all_msgs(user.id) msgs = SiteMessageUtil.get_user_all_msgs(user.id)
else: else:
msgs = SiteMessage.filter_user_msgs(user.id, has_read=is_true(has_read)) msgs = SiteMessageUtil.filter_user_msgs(user.id, has_read=is_true(has_read))
return msgs return msgs
@action(methods=[GET], detail=False, url_path='unread-total') @action(methods=[GET], detail=False, url_path='unread-total')
def unread_total(self, request, **kwargs): def unread_total(self, request, **kwargs):
user = request.user user = request.user
msgs = SiteMessage.filter_user_msgs(user.id, has_read=False) msgs = SiteMessageUtil.filter_user_msgs(user.id, has_read=False)
return Response(data={'total': msgs.count()}) return Response(data={'total': msgs.count()})
@action(methods=[PATCH], detail=False, url_path='mark-as-read') @action(methods=[PATCH], detail=False, url_path='mark-as-read')
@ -47,12 +47,12 @@ class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JmsGenericViewSet):
seri = self.get_serializer(data=request.data) seri = self.get_serializer(data=request.data)
seri.is_valid(raise_exception=True) seri.is_valid(raise_exception=True)
ids = seri.validated_data['ids'] ids = seri.validated_data['ids']
SiteMessage.mark_msgs_as_read(user.id, ids) SiteMessageUtil.mark_msgs_as_read(user.id, ids)
return Response({'detail': 'ok'}) return Response({'detail': 'ok'})
@action(methods=[POST], detail=False) @action(methods=[POST], detail=False)
def send(self, request, **kwargs): def send(self, request, **kwargs):
seri = self.get_serializer(data=request.data) seri = self.get_serializer(data=request.data)
seri.is_valid(raise_exception=True) seri.is_valid(raise_exception=True)
SiteMessage.send_msg(**seri.validated_data, sender=request.user) SiteMessageUtil.send_msg(**seri.validated_data, sender=request.user)
return Response({'detail': 'ok'}) return Response({'detail': 'ok'})

View File

@ -3,3 +3,7 @@ from django.apps import AppConfig
class NotificationsConfig(AppConfig): class NotificationsConfig(AppConfig):
name = 'notifications' name = 'notifications'
def ready(self):
from . import signals_handler
super().ready()

View File

@ -1,4 +1,4 @@
from notifications.site_msg import SiteMessage as Client from notifications.site_msg import SiteMessageUtil as Client
from .base import BackendBase from .base import BackendBase

View File

@ -17,7 +17,7 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='SiteMessage', name='SiteMessageUtil',
fields=[ fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), ('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')), ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),

View File

@ -0,0 +1,43 @@
import json
from django.utils.functional import LazyObject
from django.db.models.signals import post_save
from django.dispatch import receiver
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from common.decorator import on_transaction_commit
from .models import SiteMessage
logger = get_logger(__name__)
def new_site_msg_pub_sub():
return RedisPubSub('notifications.SiteMessageCome')
class NewSiteMsgSubPub(LazyObject):
def _setup(self):
self._wrapped = new_site_msg_pub_sub()
new_site_msg_chan = NewSiteMsgSubPub()
@receiver(post_save, sender=SiteMessage)
@on_transaction_commit
def on_site_message_create(sender, instance, created, **kwargs):
if not created:
return
logger.debug('New site msg created, publish it')
user_ids = instance.users.all().values_list('id', flat=True)
user_ids = [str(i) for i in user_ids]
data = {
'id': str(instance.id),
'subject': instance.subject,
'message': instance.message,
'users': user_ids
}
data = json.dumps(data)
new_site_msg_chan.publish(data)

View File

@ -1,11 +1,12 @@
from django.db.models import F from django.db.models import F
from django.db import transaction
from common.utils.timezone import now from common.utils.timezone import now
from users.models import User from users.models import User
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
class SiteMessage: class SiteMessageUtil:
@classmethod @classmethod
def send_msg(cls, subject, message, user_ids=(), group_ids=(), def send_msg(cls, subject, message, user_ids=(), group_ids=(),
@ -13,24 +14,24 @@ class SiteMessage:
if not any((user_ids, group_ids, is_broadcast)): if not any((user_ids, group_ids, is_broadcast)):
raise ValueError('No recipient is specified') raise ValueError('No recipient is specified')
site_msg = SiteMessageModel.objects.create( with transaction.atomic():
subject=subject, message=message, site_msg = SiteMessageModel.objects.create(
is_broadcast=is_broadcast, sender=sender, subject=subject, message=message,
) is_broadcast=is_broadcast, sender=sender,
)
if is_broadcast: if is_broadcast:
user_ids = User.objects.all().values_list('id', flat=True) user_ids = User.objects.all().values_list('id', flat=True)
else: else:
if group_ids: if group_ids:
site_msg.groups.add(*group_ids) site_msg.groups.add(*group_ids)
user_ids_from_group = User.groups.through.objects.filter( user_ids_from_group = User.groups.through.objects.filter(
usergroup_id__in=group_ids usergroup_id__in=group_ids
).values_list('user_id', flat=True) ).values_list('user_id', flat=True)
user_ids = [*user_ids, *user_ids_from_group]
user_ids = [*user_ids, *user_ids_from_group] site_msg.users.add(*user_ids)
site_msg.users.add(*user_ids)
@classmethod @classmethod
def get_user_all_msgs(cls, user_id): def get_user_all_msgs(cls, user_id):
@ -72,14 +73,14 @@ class SiteMessage:
@classmethod @classmethod
def mark_msgs_as_read(cls, user_id, msg_ids): def mark_msgs_as_read(cls, user_id, msg_ids):
sitemsg_users = SiteMessageUsers.objects.filter( site_msg_users = SiteMessageUsers.objects.filter(
user_id=user_id, sitemessage_id__in=msg_ids, user_id=user_id, sitemessage_id__in=msg_ids,
has_read=False has_read=False
) )
for sitemsg_user in sitemsg_users: for site_msg_user in site_msg_users:
sitemsg_user.has_read = True site_msg_user.has_read = True
sitemsg_user.read_at = now() site_msg_user.read_at = now()
SiteMessageUsers.objects.bulk_update( SiteMessageUsers.objects.bulk_update(
sitemsg_users, fields=('has_read', 'read_at')) site_msg_users, fields=('has_read', 'read_at'))

View File

@ -0,0 +1,9 @@
from django.urls import path
from .. import ws
app_name = 'notifications'
urlpatterns = [
path('ws/notifications/site-msg/', ws.SiteMsgWebsocket, name='site-msg-ws'),
]

70
apps/notifications/ws.py Normal file
View File

@ -0,0 +1,70 @@
import threading
import json
from channels.generic.websocket import JsonWebsocketConsumer
from common.utils import get_logger
from .models import SiteMessage
from .site_msg import SiteMessageUtil
from .signals_handler import new_site_msg_chan
logger = get_logger(__name__)
class SiteMsgWebsocket(JsonWebsocketConsumer):
disconnected = False
refresh_every_seconds = 10
def connect(self):
user = self.scope["user"]
if user.is_authenticated:
self.accept()
thread = threading.Thread(target=self.unread_site_msg_count)
thread.start()
else:
self.close()
def receive(self, text_data=None, bytes_data=None, **kwargs):
data = json.loads(text_data)
refresh_every_seconds = data.get('refresh_every_seconds')
try:
refresh_every_seconds = int(refresh_every_seconds)
except Exception as e:
logger.error(e)
return
if refresh_every_seconds > 0:
self.refresh_every_seconds = refresh_every_seconds
def send_unread_msg_count(self):
user_id = self.scope["user"].id
unread_count = SiteMessageUtil.get_user_unread_msgs_count(user_id)
logger.debug('Send unread count to user: {} {}'.format(user_id, unread_count))
self.send_json({'type': 'unread_count', 'unread_count': unread_count})
def unread_site_msg_count(self):
user_id = str(self.scope["user"].id)
self.send_unread_msg_count()
while not self.disconnected:
subscribe = new_site_msg_chan.subscribe()
for message in subscribe.listen():
if message['type'] != 'message':
continue
try:
msg = json.loads(message['data'].decode())
logger.debug('New site msg recv, may be mine: {}'.format(msg))
if not msg:
continue
users = msg.get('users', [])
logger.debug('Message users: {}'.format(users))
if user_id in users:
self.send_unread_msg_count()
except json.JSONDecoder as e:
logger.debug('Decode json error: ', e)
def disconnect(self, close_code):
self.disconnected = True
self.close()