diff --git a/apps/applications/api/__init__.py b/apps/applications/api/__init__.py index 68f0d1803..a11b4966a 100644 --- a/apps/applications/api/__init__.py +++ b/apps/applications/api/__init__.py @@ -1,3 +1,4 @@ from .application import * +from .application_user import * from .mixin import * from .remote_app import * diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index 84426b6b2..867a8ddd3 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -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 diff --git a/apps/applications/api/application_user.py b/apps/applications/api/application_user.py new file mode 100644 index 000000000..93bbc29a5 --- /dev/null +++ b/apps/applications/api/application_user.py @@ -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() diff --git a/apps/applications/hands.py b/apps/applications/hands.py index ee13e589e..4987d5948 100644 --- a/apps/applications/hands.py +++ b/apps/applications/hands.py @@ -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 diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index f7b541580..c9374181d 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -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 diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index bff8c270a..94c55c1a8 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -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'] diff --git a/apps/applications/urls/api_urls.py b/apps/applications/urls/api_urls.py index 9ca50d32c..fb5d08228 100644 --- a/apps/applications/urls/api_urls.py +++ b/apps/applications/urls/api_urls.py @@ -14,7 +14,8 @@ router.register(r'applications', api.ApplicationViewSet, 'application') urlpatterns = [ path('remote-apps//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') ] diff --git a/apps/audits/api.py b/apps/audits/api.py index 83caf0db9..ed44bdc1a 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -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): diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index d7ab3e996..7bab342ee 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -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'] diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index 930a1f03d..5f94fb42a 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -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) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 76d6e934d..1ac5435be 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -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) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 9da3bbbc3..308c441a2 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -228,3 +228,11 @@ class DingTalkAuthentication(ModelBackend): def authenticate(self, request, **kwargs): pass + + +class AuthorizationTokenAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + def authenticate(self, request, **kwargs): + pass diff --git a/apps/jumpserver/routing.py b/apps/jumpserver/routing.py index d76f1ccee..5deae804e 100644 --- a/apps/jumpserver/routing.py +++ b/apps/jumpserver/routing.py @@ -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( diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 502146f13..a4b2fb296 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -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: diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 268bafa44..ef69cec8e 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -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', diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index c2ffea6ec..43d0e6cb0 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -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()), ] diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a3df15a78..f1e5b13b4 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index c6787d382..0acdf61d9 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-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 \n" "Language-Team: JumpServer team\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 "启用钉钉认证" diff --git a/apps/notifications/api/site_msgs.py b/apps/notifications/api/site_msgs.py index 6ee856922..2f8ba7e15 100644 --- a/apps/notifications/api/site_msgs.py +++ b/apps/notifications/api/site_msgs.py @@ -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'}) diff --git a/apps/notifications/apps.py b/apps/notifications/apps.py index 9c260e0b1..f14a8ebe9 100644 --- a/apps/notifications/apps.py +++ b/apps/notifications/apps.py @@ -3,3 +3,7 @@ from django.apps import AppConfig class NotificationsConfig(AppConfig): name = 'notifications' + + def ready(self): + from . import signals_handler + super().ready() diff --git a/apps/notifications/backends/site_msg.py b/apps/notifications/backends/site_msg.py index 33032843a..0f7468f48 100644 --- a/apps/notifications/backends/site_msg.py +++ b/apps/notifications/backends/site_msg.py @@ -1,4 +1,4 @@ -from notifications.site_msg import SiteMessage as Client +from notifications.site_msg import SiteMessageUtil as Client from .base import BackendBase diff --git a/apps/notifications/migrations/0001_initial.py b/apps/notifications/migrations/0001_initial.py index ebe79f304..8e55bb305 100644 --- a/apps/notifications/migrations/0001_initial.py +++ b/apps/notifications/migrations/0001_initial.py @@ -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')), diff --git a/apps/notifications/signals_handler.py b/apps/notifications/signals_handler.py new file mode 100644 index 000000000..13ebdc4bc --- /dev/null +++ b/apps/notifications/signals_handler.py @@ -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) diff --git a/apps/notifications/site_msg.py b/apps/notifications/site_msg.py index b78d3c7f4..1a5c9dc23 100644 --- a/apps/notifications/site_msg.py +++ b/apps/notifications/site_msg.py @@ -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')) diff --git a/apps/notifications/urls/notifications.py b/apps/notifications/urls/api_urls.py similarity index 100% rename from apps/notifications/urls/notifications.py rename to apps/notifications/urls/api_urls.py diff --git a/apps/notifications/urls/ws_urls.py b/apps/notifications/urls/ws_urls.py new file mode 100644 index 000000000..dfd457e52 --- /dev/null +++ b/apps/notifications/urls/ws_urls.py @@ -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'), +] \ No newline at end of file diff --git a/apps/notifications/ws.py b/apps/notifications/ws.py new file mode 100644 index 000000000..cbbb25d2d --- /dev/null +++ b/apps/notifications/ws.py @@ -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()