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_user import *
from .mixin import *
from .remote_app import *

View File

@ -2,18 +2,13 @@
#
from orgs.mixins.api import OrgBulkModelViewSet
from rest_framework import generics
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin
from .. import models, serializers
from ..hands import IsOrgAdminOrAppUser
from .. import serializers
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):
@ -22,29 +17,3 @@ class ApplicationViewSet(OrgBulkModelViewSet):
search_fields = filterset_fields
permission_classes = (IsOrgAdminOrAppUser,)
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

View File

@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from assets.models import Asset
from assets.models import Asset, SystemUser
from .. import const
@ -68,3 +68,8 @@ class Application(CommonModelMixin, OrgModelMixin):
raise ValueError("Remote App not has asset attr")
asset = Asset.objects.filter(id=asset_id).first()
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 common.drf.serializers import MethodSerializer
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
from assets.serializers import SystemUserSerializer
from .. import models
__all__ = [
'ApplicationSerializer', 'ApplicationSerializerMixin',
'ApplicationUserSerializer', 'ApplicationUserWithAuthInfoSerializer'
]
@ -66,3 +67,42 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
_attrs.update(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 = [
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_start', ('date_from', 'date_to'))
]
filterset_fields = ['user__name', 'command', 'run_as__name', 'is_finished']
search_fields = ['command', 'user__name', 'run_as__name']
filterset_fields = [
'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']
def get_queryset(self):

View File

@ -82,7 +82,7 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
model = CommandExecution
fields_mini = ['id']
fields_small = fields_mini + [
'run_as', 'command', 'user', 'is_finished',
'run_as', 'command', 'is_finished', 'user',
'date_start', 'result', 'is_success', 'org_id'
]
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_MODEL] = _('Password')
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_DINGTALK] = _('DingTalk')
return backend_label_mapping
@ -156,12 +157,13 @@ def get_login_backend(request):
return backend_label
def generate_data(username, request):
def generate_data(username, request, login_type=None):
user_agent = request.META.get('HTTP_USER_AGENT', '')
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')
else:
if login_type is None:
login_type = 'W'
data = {
@ -176,9 +178,9 @@ def generate_data(username, request):
@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))
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})
write_login_log(**data)

View File

@ -6,12 +6,14 @@ from django.conf import settings
from django.core.cache import cache
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.utils.translation import ugettext as _
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
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.drf.api import SerializerMixin2
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
@ -51,10 +53,6 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
return True
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:
raise PermissionDenied('Only super user can create user token')
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:
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
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
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)
if asset:
@ -252,6 +262,9 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
data['type'] = 'application'
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)
return Response(data=serializer.data, status=200)

View File

@ -228,3 +228,11 @@ class DingTalkAuthentication(ModelBackend):
def authenticate(self, request, **kwargs):
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 ops.urls.ws_urls import urlpatterns as ops_urlpatterns
from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns
urlpatterns = []
urlpatterns += ops_urlpatterns
urlpatterns += ops_urlpatterns \
+ notifications_urlpatterns
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(

View File

@ -130,10 +130,12 @@ AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend'
AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication'
AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication'
AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthentication'
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:

View File

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

View File

@ -23,7 +23,7 @@ api_v1 = [
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
path('notifications/', include('notifications.urls.notifications', namespace='api-notifications')),
path('notifications/', include('notifications.urls.api_urls', namespace='api-notifications')),
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
]

Binary file not shown.

View File

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

View File

@ -10,7 +10,7 @@ from ..serializers import (
SiteMessageDetailSerializer, SiteMessageIdsSerializer,
SiteMessageSendSerializer,
)
from ..site_msg import SiteMessage
from ..site_msg import SiteMessageUtil
from ..filters import SiteMsgFilter
__all__ = ('SiteMessageViewSet', )
@ -30,15 +30,15 @@ class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JmsGenericViewSet):
has_read = self.request.query_params.get('has_read')
if has_read is None:
msgs = SiteMessage.get_user_all_msgs(user.id)
msgs = SiteMessageUtil.get_user_all_msgs(user.id)
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
@action(methods=[GET], detail=False, url_path='unread-total')
def unread_total(self, request, **kwargs):
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()})
@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.is_valid(raise_exception=True)
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'})
@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)
SiteMessageUtil.send_msg(**seri.validated_data, sender=request.user)
return Response({'detail': 'ok'})

View File

@ -3,3 +3,7 @@ from django.apps import AppConfig
class NotificationsConfig(AppConfig):
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

View File

@ -17,7 +17,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='SiteMessage',
name='SiteMessageUtil',
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')),

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 import transaction
from common.utils.timezone import now
from users.models import User
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
class SiteMessage:
class SiteMessageUtil:
@classmethod
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)):
raise ValueError('No recipient is specified')
site_msg = SiteMessageModel.objects.create(
subject=subject, message=message,
is_broadcast=is_broadcast, sender=sender,
)
with transaction.atomic():
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)
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_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]
user_ids = [*user_ids, *user_ids_from_group]
site_msg.users.add(*user_ids)
site_msg.users.add(*user_ids)
@classmethod
def get_user_all_msgs(cls, user_id):
@ -72,14 +73,14 @@ class SiteMessage:
@classmethod
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,
has_read=False
)
for sitemsg_user in sitemsg_users:
sitemsg_user.has_read = True
sitemsg_user.read_at = now()
for site_msg_user in site_msg_users:
site_msg_user.has_read = True
site_msg_user.read_at = now()
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()