mirror of https://github.com/jumpserver/jumpserver
commit
f673fed706
|
@ -11,5 +11,5 @@ class LoginAclFilter(BaseFilterSet):
|
|||
class Meta:
|
||||
model = LoginACL
|
||||
fields = (
|
||||
'name', 'user', 'user_display'
|
||||
'name', 'user', 'user_display', 'action'
|
||||
)
|
||||
|
|
|
@ -7,6 +7,8 @@ from acls.models import LoginACL
|
|||
LOGIN_CONFIRM_ZH = '登录复核'
|
||||
LOGIN_CONFIRM_EN = 'Login confirm'
|
||||
|
||||
DEFAULT_TIME_PERIODS = [{'id': i, 'value': '00:00~00:00'} for i in range(7)]
|
||||
|
||||
|
||||
def has_zh(name: str) -> bool:
|
||||
for i in name:
|
||||
|
@ -31,7 +33,8 @@ def migrate_login_confirm(apps, schema_editor):
|
|||
'user': user,
|
||||
'name': f'{user.name}-{login_confirm} ({date_created})',
|
||||
'created_by': instance.created_by,
|
||||
'action': LoginACL.ActionChoices.confirm
|
||||
'action': LoginACL.ActionChoices.confirm,
|
||||
'rules': {'ip_group': ['*'], 'time_period': DEFAULT_TIME_PERIODS}
|
||||
}
|
||||
instance = login_acl_model.objects.create(**data)
|
||||
instance.reviewers.set(reviewers)
|
||||
|
@ -39,11 +42,10 @@ def migrate_login_confirm(apps, schema_editor):
|
|||
|
||||
def migrate_ip_group(apps, schema_editor):
|
||||
login_acl_model = apps.get_model("acls", "LoginACL")
|
||||
default_time_periods = [{'id': i, 'value': '00:00~00:00'} for i in range(7)]
|
||||
updates = list()
|
||||
with transaction.atomic():
|
||||
for instance in login_acl_model.objects.exclude(action=LoginACL.ActionChoices.confirm):
|
||||
instance.rules = {'ip_group': instance.ip_group, 'time_period': default_time_periods}
|
||||
instance.rules = {'ip_group': instance.ip_group, 'time_period': DEFAULT_TIME_PERIODS}
|
||||
updates.append(instance)
|
||||
login_acl_model.objects.bulk_update(updates, ['rules', ])
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from rest_framework.decorators import action
|
|||
from rest_framework.response import Response
|
||||
|
||||
from common.tree import TreeNodeSerializer
|
||||
from common.mixins.views import SuggestionMixin
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
from .. import serializers
|
||||
from ..models import Application
|
||||
|
|
|
@ -21,6 +21,8 @@ class AdminUserViewSet(OrgBulkModelViewSet):
|
|||
search_fields = filterset_fields
|
||||
serializer_class = serializers.AdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().filter(type=SystemUser.Type.admin)
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.db.models import Q
|
|||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
||||
from common.mixins.views import SuggestionMixin
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from users.models import User, UserGroup
|
||||
from users.serializers import UserSerializer, UserGroupSerializer
|
||||
from users.filters import UserFilter
|
||||
|
@ -50,6 +50,7 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
|
|||
}
|
||||
search_fields = ("hostname", "ip")
|
||||
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
||||
ordering = ('hostname', )
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetSerializer,
|
||||
'suggestion': serializers.MiniAssetSerializer
|
||||
|
|
|
@ -22,6 +22,8 @@ class DomainViewSet(OrgBulkModelViewSet):
|
|||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DomainSerializer
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.query_params.get('gateway'):
|
||||
|
|
|
@ -6,7 +6,7 @@ from common.utils import get_logger
|
|||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from common.mixins.views import SuggestionMixin
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from ..models import SystemUser, Asset
|
||||
from .. import serializers
|
||||
|
@ -41,6 +41,8 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
|||
'default': serializers.SystemUserSerializer,
|
||||
'suggestion': serializers.MiniSystemUserSerializer
|
||||
}
|
||||
ordering_fields = ('name', 'protocol')
|
||||
ordering = ('name', )
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
|
||||
|
@ -73,7 +75,7 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
|
|||
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_404(SystemUser, pk=pk)
|
||||
instance.set_temp_auth(instance_id, user, data)
|
||||
instance.set_temp_auth(instance_id, user.id, data)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
|
|
|
@ -185,10 +185,18 @@ class BaseUser(OrgModelMixin, AuthMixin):
|
|||
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
|
||||
ASSET_USER_CACHE_TIME = 600
|
||||
|
||||
APPS_AMOUNT_CACHE_KEY = "APP_USER_{}_APPS_AMOUNT"
|
||||
APP_USER_CACHE_TIME = 600
|
||||
|
||||
def get_related_assets(self):
|
||||
assets = self.assets.filter(org_id=self.org_id)
|
||||
return assets
|
||||
|
||||
def get_related_apps(self):
|
||||
from applications.models import Account
|
||||
apps = Account.objects.filter(systemuser=self)
|
||||
return apps
|
||||
|
||||
def get_username(self):
|
||||
return self.username
|
||||
|
||||
|
@ -201,6 +209,15 @@ class BaseUser(OrgModelMixin, AuthMixin):
|
|||
cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME)
|
||||
return cached
|
||||
|
||||
@property
|
||||
def apps_amount(self):
|
||||
cache_key = self.APPS_AMOUNT_CACHE_KEY.format(self.id)
|
||||
cached = cache.get(cache_key)
|
||||
if not cached:
|
||||
cached = self.get_related_apps().count()
|
||||
cache.set(cache_key, cached, self.APP_USER_CACHE_TIME)
|
||||
return cached
|
||||
|
||||
def expire_assets_amount(self):
|
||||
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
|
||||
cache.delete(cache_key)
|
||||
|
|
|
@ -103,16 +103,23 @@ class AuthMixin:
|
|||
password = cache.get(key)
|
||||
return password
|
||||
|
||||
def load_tmp_auth_if_has(self, asset_or_app_id, user):
|
||||
if not asset_or_app_id or not user:
|
||||
return
|
||||
def _clean_auth_info_if_manual_login_mode(self):
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
self.public_key = ''
|
||||
|
||||
def _load_tmp_auth_if_has(self, asset_or_app_id, user_id):
|
||||
if self.login_mode != self.LOGIN_MANUAL:
|
||||
return
|
||||
|
||||
auth = self.get_temp_auth(asset_or_app_id, user)
|
||||
if not asset_or_app_id or not user_id:
|
||||
return
|
||||
|
||||
auth = self.get_temp_auth(asset_or_app_id, user_id)
|
||||
if not auth:
|
||||
return
|
||||
|
||||
username = auth.get('username')
|
||||
password = auth.get('password')
|
||||
|
||||
|
@ -122,17 +129,11 @@ class AuthMixin:
|
|||
self.password = password
|
||||
|
||||
def load_app_more_auth(self, app_id=None, user_id=None):
|
||||
from users.models import User
|
||||
|
||||
self._clean_auth_info_if_manual_login_mode()
|
||||
# 加载临时认证信息
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
if not user_id:
|
||||
self._load_tmp_auth_if_has(app_id, user_id)
|
||||
return
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if not user:
|
||||
return
|
||||
self.load_tmp_auth_if_has(app_id, user)
|
||||
|
||||
def load_asset_special_auth(self, asset, username=''):
|
||||
"""
|
||||
|
@ -152,34 +153,25 @@ class AuthMixin:
|
|||
|
||||
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
|
||||
from users.models import User
|
||||
|
||||
self._clean_auth_info_if_manual_login_mode()
|
||||
# 加载临时认证信息
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
|
||||
asset = None
|
||||
if asset_id:
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
# 没有资产就没有必要继续了
|
||||
if not asset:
|
||||
logger.debug('Asset not found, pass')
|
||||
self._load_tmp_auth_if_has(asset_id, user_id)
|
||||
return
|
||||
|
||||
user = None
|
||||
if user_id:
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
|
||||
_username = self.username
|
||||
# 更新用户名
|
||||
user = get_object_or_none(User, pk=user_id) if user_id else None
|
||||
if self.username_same_with_user:
|
||||
if user and not username:
|
||||
_username = user.username
|
||||
else:
|
||||
_username = username
|
||||
self.username = _username
|
||||
|
||||
# 加载某个资产的特殊配置认证信息
|
||||
self.load_asset_special_auth(asset, _username)
|
||||
self.load_tmp_auth_if_has(asset_id, user)
|
||||
asset = get_object_or_none(Asset, pk=asset_id) if asset_id else None
|
||||
if not asset:
|
||||
logger.debug('Asset not found, pass')
|
||||
return
|
||||
self.load_asset_special_auth(asset, self.username)
|
||||
|
||||
|
||||
class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
|
||||
|
|
|
@ -26,6 +26,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True)
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint'))
|
||||
applications_amount = serializers.IntegerField(source='apps_amount', label=_('Apps amount'))
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
|
@ -39,7 +40,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
'username_same_with_user', 'auto_push', 'auto_generate_key',
|
||||
'date_created', 'date_updated', 'comment', 'created_by',
|
||||
]
|
||||
fields_m2m = ['cmd_filters', 'assets_amount', 'nodes']
|
||||
fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes']
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {
|
||||
|
@ -123,7 +124,8 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
return ''
|
||||
return home
|
||||
|
||||
def validate_sftp_root(self, value):
|
||||
@staticmethod
|
||||
def validate_sftp_root(value):
|
||||
if value in ['home', 'tmp']:
|
||||
return value
|
||||
if not value.startswith('/'):
|
||||
|
@ -131,19 +133,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
raise serializers.ValidationError(error)
|
||||
return value
|
||||
|
||||
def validate_admin_user(self, attrs):
|
||||
if self.instance:
|
||||
tp = self.instance.type
|
||||
else:
|
||||
tp = attrs.get('type')
|
||||
if tp != SystemUser.Type.admin:
|
||||
return attrs
|
||||
attrs['protocol'] = SystemUser.Protocol.ssh
|
||||
attrs['login_mode'] = SystemUser.LOGIN_AUTO
|
||||
attrs['username_same_with_user'] = False
|
||||
attrs['auto_push'] = False
|
||||
return attrs
|
||||
|
||||
def validate_password(self, password):
|
||||
super().validate_password(password)
|
||||
auto_gen_key = self.get_initial_value("auto_generate_key", False)
|
||||
|
@ -155,7 +144,20 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
raise serializers.ValidationError(_("Password or private key required"))
|
||||
return password
|
||||
|
||||
def validate_gen_key(self, attrs):
|
||||
def _validate_admin_user(self, attrs):
|
||||
if self.instance:
|
||||
tp = self.instance.type
|
||||
else:
|
||||
tp = attrs.get('type')
|
||||
if tp != SystemUser.Type.admin:
|
||||
return attrs
|
||||
attrs['protocol'] = SystemUser.Protocol.ssh
|
||||
attrs['login_mode'] = SystemUser.LOGIN_AUTO
|
||||
attrs['username_same_with_user'] = False
|
||||
attrs['auto_push'] = False
|
||||
return attrs
|
||||
|
||||
def _validate_gen_key(self, attrs):
|
||||
username = attrs.get("username", "manual")
|
||||
auto_gen_key = attrs.pop("auto_generate_key", False)
|
||||
protocol = attrs.get("protocol")
|
||||
|
@ -179,16 +181,30 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
attrs["public_key"] = public_key
|
||||
return attrs
|
||||
|
||||
def _validate_login_mode(self, attrs):
|
||||
if 'login_mode' in attrs:
|
||||
login_mode = attrs['login_mode']
|
||||
else:
|
||||
login_mode = self.instance.login_mode if self.instance else SystemUser.LOGIN_AUTO
|
||||
|
||||
if login_mode == SystemUser.LOGIN_MANUAL:
|
||||
attrs['password'] = ''
|
||||
attrs['private_key'] = ''
|
||||
attrs['public_key'] = ''
|
||||
|
||||
return attrs
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = self.validate_admin_user(attrs)
|
||||
attrs = self.validate_gen_key(attrs)
|
||||
attrs = self._validate_admin_user(attrs)
|
||||
attrs = self._validate_gen_key(attrs)
|
||||
attrs = self._validate_login_mode(attrs)
|
||||
return attrs
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset\
|
||||
.annotate(assets_amount=Count("assets"))\
|
||||
.annotate(assets_amount=Count("assets")) \
|
||||
.prefetch_related('nodes', 'cmd_filters')
|
||||
return queryset
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ 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 SerializerMixin
|
||||
from common.mixins.api import SerializerMixin
|
||||
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from common.http import is_true
|
||||
|
@ -150,15 +150,19 @@ class ClientProtocolMixin:
|
|||
def get_client_protocol_data(self, serializer):
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
protocol = system_user.protocol
|
||||
username = user.username
|
||||
name = ''
|
||||
if protocol == 'rdp':
|
||||
name, config = self.get_rdp_file_content(serializer)
|
||||
elif protocol == 'vnc':
|
||||
raise HttpResponse(status=404, data={"error": "VNC not support"})
|
||||
else:
|
||||
config = 'ssh://system_user@asset@user@jumpserver-ssh'
|
||||
filename = "{}-{}-jumpserver".format(username, name)
|
||||
data = {
|
||||
"filename": filename,
|
||||
"protocol": system_user.protocol,
|
||||
"username": user.username,
|
||||
"username": username,
|
||||
"config": config
|
||||
}
|
||||
return data
|
||||
|
|
|
@ -134,6 +134,10 @@ class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFail
|
|||
times_remainder = util.get_remainder_times()
|
||||
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
|
||||
|
||||
if times_remainder < 1:
|
||||
self.msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
|
||||
return
|
||||
|
||||
default_msg = invalid_login_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import random
|
|||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms import SMS
|
||||
from common.sdk.sms import SMS
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
|
||||
|
|
|
@ -12,7 +12,5 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
<small>
|
||||
{% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %}
|
||||
</small>
|
||||
{% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %}
|
||||
</p>
|
|
@ -1,15 +1,19 @@
|
|||
{% load i18n %}
|
||||
{% trans 'Hello' %} {{ user.name }},
|
||||
<br>
|
||||
{% trans 'Please click the link below to reset your password, if not your request, concern your account security' %}
|
||||
<br>
|
||||
<br>
|
||||
<a href="{{ rest_password_url }}?token={{ rest_password_token}}" class='showLink'>{% trans 'Click here reset password' %}</a>
|
||||
<br>
|
||||
<br>
|
||||
{% trans 'This link is valid for 1 hour. After it expires,' %} <a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a>
|
||||
<br>
|
||||
---
|
||||
<br>
|
||||
<a href="{{ login_url }}">{% trans 'Login direct' %}</a>
|
||||
<br>
|
||||
<p>
|
||||
{% trans 'Hello' %} {{ user.name }},
|
||||
</p>
|
||||
<p>
|
||||
{% trans 'Please click the link below to reset your password, if not your request, concern your account security' %}
|
||||
<br>
|
||||
<br>
|
||||
<a href="{{ rest_password_url }}?token={{ rest_password_token}}" class='showLink' target="_blank">
|
||||
{% trans 'Click here reset password' %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% trans 'This link is valid for 1 hour. After it expires' %}
|
||||
<a href="{{ forget_password_url }}?email={{ user.email }}">
|
||||
{% trans 'request new one' %}
|
||||
</a>
|
||||
</p>
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
{% load i18n %}
|
||||
|
||||
<p>{% trans 'Hello' %}: {{ name }},</p>
|
||||
<p>{% trans 'Hello' %} {{ name }},</p>
|
||||
|
||||
<p>
|
||||
{% trans 'Your password has just been successfully updated.' %}
|
||||
{% trans 'Your password has just been successfully updated' %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans 'IP' %}: {{ ip_address }} <br />
|
||||
{% trans 'Browser' %}: {{ browser }}
|
||||
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br />
|
||||
<b>{% trans 'Browser' %}:</b> {{ browser }}
|
||||
</p>
|
||||
<p>---</p>
|
||||
<p>
|
||||
<small>
|
||||
{% trans 'If the password update was not initiated by you, your account may have security issues.' %} <br />
|
||||
{% trans 'If you have any questions, you can contact the administrator.' %}
|
||||
</small>
|
||||
{% trans 'If the password update was not initiated by you, your account may have security issues' %} <br />
|
||||
{% trans 'If you have any questions, you can contact the administrator' %}
|
||||
</p>
|
||||
|
|
|
@ -14,11 +14,11 @@ from users.models import User
|
|||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.message.backends.dingtalk import URL
|
||||
from common.sdk.im.dingtalk import URL
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
from common.message.backends.dingtalk import DingTalk
|
||||
from common.sdk.im.dingtalk import DingTalk
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ from common.utils import get_logger, FlashMessageUtil
|
|||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from common.message.backends.feishu import FeiShu, URL
|
||||
from common.sdk.im.feishu import FeiShu, URL
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ from users.models import User
|
|||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.message.backends.wecom import URL
|
||||
from common.message.backends.wecom import WeCom
|
||||
from common.sdk.im.wecom import URL
|
||||
from common.sdk.im.wecom import WeCom
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
|
|
|
@ -2,19 +2,10 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelV
|
|||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from ..mixins.api import (
|
||||
SerializerMixin, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin,
|
||||
RelationMixin, AllowBulkDestroyMixin, RenderToJsonMixin,
|
||||
RelationMixin, AllowBulkDestroyMixin, CommonMixin
|
||||
)
|
||||
|
||||
|
||||
class CommonMixin(SerializerMixin,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
RenderToJsonMixin):
|
||||
pass
|
||||
|
||||
|
||||
class JMSGenericViewSet(CommonMixin, GenericViewSet):
|
||||
pass
|
||||
|
||||
|
|
|
@ -1,347 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from hashlib import md5
|
||||
from threading import Thread
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.core.cache import cache
|
||||
from django.http import JsonResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.const.http import POST
|
||||
from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
|
||||
from ..utils import lazyproperty
|
||||
|
||||
__all__ = [
|
||||
'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin',
|
||||
'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin',
|
||||
'SerializerMixin', 'AllowBulkDestroyMixin', 'PaginatedResponseMixin'
|
||||
]
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class JSONResponseMixin(object):
|
||||
"""JSON mixin"""
|
||||
@staticmethod
|
||||
def render_json_response(context):
|
||||
return JsonResponse(context)
|
||||
|
||||
|
||||
# SerializerMixin
|
||||
# ----------------------
|
||||
|
||||
|
||||
class RenderToJsonMixin:
|
||||
@action(methods=[POST], detail=False, url_path='render-to-json')
|
||||
def render_to_json(self, request: Request):
|
||||
data = {
|
||||
'title': (),
|
||||
'data': request.data,
|
||||
}
|
||||
|
||||
jms_context = getattr(request, 'jms_context', {})
|
||||
column_title_field_pairs = jms_context.get('column_title_field_pairs', ())
|
||||
data['title'] = column_title_field_pairs
|
||||
|
||||
if isinstance(request.data, (list, tuple)) and not any(request.data):
|
||||
error = _("Request file format may be wrong")
|
||||
return Response(data={"error": error}, status=400)
|
||||
return Response(data=data)
|
||||
|
||||
|
||||
class SerializerMixin:
|
||||
""" 根据用户请求动作的不同,获取不同的 `serializer_class `"""
|
||||
|
||||
action: str
|
||||
request: Request
|
||||
|
||||
serializer_classes = None
|
||||
single_actions = ['put', 'retrieve', 'patch']
|
||||
|
||||
def get_serializer_class_by_view_action(self):
|
||||
if not hasattr(self, 'serializer_classes'):
|
||||
return None
|
||||
if not isinstance(self.serializer_classes, dict):
|
||||
return None
|
||||
|
||||
view_action = self.request.query_params.get('action') or self.action or 'list'
|
||||
serializer_class = self.serializer_classes.get(view_action)
|
||||
|
||||
if serializer_class is None:
|
||||
view_method = self.request.method.lower()
|
||||
serializer_class = self.serializer_classes.get(view_method)
|
||||
|
||||
if serializer_class is None and view_action in self.single_actions:
|
||||
serializer_class = self.serializer_classes.get('single')
|
||||
if serializer_class is None:
|
||||
serializer_class = self.serializer_classes.get('display')
|
||||
if serializer_class is None:
|
||||
serializer_class = self.serializer_classes.get('default')
|
||||
return serializer_class
|
||||
|
||||
def get_serializer_class(self):
|
||||
serializer_class = self.get_serializer_class_by_view_action()
|
||||
if serializer_class is None:
|
||||
serializer_class = super().get_serializer_class()
|
||||
return serializer_class
|
||||
|
||||
|
||||
class ExtraFilterFieldsMixin:
|
||||
"""
|
||||
额外的 api filter
|
||||
"""
|
||||
default_added_filters = [CustomFilter, IDSpmFilter, IDInFilter]
|
||||
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
|
||||
extra_filter_fields = []
|
||||
extra_filter_backends = []
|
||||
|
||||
def get_filter_backends(self):
|
||||
if self.filter_backends != self.__class__.filter_backends:
|
||||
return self.filter_backends
|
||||
backends = list(chain(
|
||||
self.filter_backends,
|
||||
self.default_added_filters,
|
||||
self.extra_filter_backends
|
||||
))
|
||||
return backends
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
for backend in self.get_filter_backends():
|
||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||
return queryset
|
||||
|
||||
|
||||
class PaginatedResponseMixin:
|
||||
def get_paginated_response_with_query_set(self, queryset):
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin, RenderToJsonMixin):
|
||||
pass
|
||||
|
||||
|
||||
class InterceptMixin:
|
||||
"""
|
||||
Hack默认的dispatch, 让用户可以实现 self.do
|
||||
"""
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
request = self.initialize_request(request, *args, **kwargs)
|
||||
self.request = request
|
||||
self.headers = self.default_response_headers # deprecate?
|
||||
|
||||
try:
|
||||
self.initial(request, *args, **kwargs)
|
||||
|
||||
# Get the appropriate handler method
|
||||
if request.method.lower() in self.http_method_names:
|
||||
handler = getattr(self, request.method.lower(),
|
||||
self.http_method_not_allowed)
|
||||
else:
|
||||
handler = self.http_method_not_allowed
|
||||
|
||||
response = self.do(handler, request, *args, **kwargs)
|
||||
|
||||
except Exception as exc:
|
||||
response = self.handle_exception(exc)
|
||||
|
||||
self.response = self.finalize_response(request, response, *args, **kwargs)
|
||||
return self.response
|
||||
|
||||
|
||||
class AsyncApiMixin(InterceptMixin):
|
||||
def get_request_user_id(self):
|
||||
user = self.request.user
|
||||
if hasattr(user, 'id'):
|
||||
return str(user.id)
|
||||
return ''
|
||||
|
||||
@lazyproperty
|
||||
def async_cache_key(self):
|
||||
method = self.request.method
|
||||
path = self.get_request_md5()
|
||||
user = self.get_request_user_id()
|
||||
key = '{}_{}_{}'.format(method, path, user)
|
||||
return key
|
||||
|
||||
def get_request_md5(self):
|
||||
path = self.request.path
|
||||
query = {k: v for k, v in self.request.GET.items()}
|
||||
query.pop("_", None)
|
||||
query.pop('refresh', None)
|
||||
query = "&".join(["{}={}".format(k, v) for k, v in query.items()])
|
||||
full_path = "{}?{}".format(path, query)
|
||||
return md5(full_path.encode()).hexdigest()
|
||||
|
||||
@lazyproperty
|
||||
def initial_data(self):
|
||||
data = {
|
||||
"status": "running",
|
||||
"start_time": time.time(),
|
||||
"key": self.async_cache_key,
|
||||
}
|
||||
return data
|
||||
|
||||
def get_cache_data(self):
|
||||
key = self.async_cache_key
|
||||
if self.is_need_refresh():
|
||||
cache.delete(key)
|
||||
return None
|
||||
data = cache.get(key)
|
||||
return data
|
||||
|
||||
def do(self, handler, *args, **kwargs):
|
||||
if not self.is_need_async():
|
||||
return handler(*args, **kwargs)
|
||||
resp = self.do_async(handler, *args, **kwargs)
|
||||
return resp
|
||||
|
||||
def is_need_refresh(self):
|
||||
if self.request.GET.get("refresh"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_need_async(self):
|
||||
return False
|
||||
|
||||
def do_async(self, handler, *args, **kwargs):
|
||||
data = self.get_cache_data()
|
||||
if not data:
|
||||
t = Thread(
|
||||
target=self.do_in_thread,
|
||||
args=(handler, *args),
|
||||
kwargs=kwargs
|
||||
)
|
||||
t.start()
|
||||
resp = Response(self.initial_data)
|
||||
return resp
|
||||
status = data.get("status")
|
||||
resp = data.get("resp")
|
||||
if status == "ok" and resp:
|
||||
resp = Response(**resp)
|
||||
else:
|
||||
resp = Response(data)
|
||||
return resp
|
||||
|
||||
def do_in_thread(self, handler, *args, **kwargs):
|
||||
key = self.async_cache_key
|
||||
data = self.initial_data
|
||||
cache.set(key, data, 600)
|
||||
try:
|
||||
response = handler(*args, **kwargs)
|
||||
data["status"] = "ok"
|
||||
data["resp"] = {
|
||||
"data": response.data,
|
||||
"status": response.status_code
|
||||
}
|
||||
cache.set(key, data, 600)
|
||||
except Exception as e:
|
||||
data["error"] = str(e)
|
||||
data["status"] = "error"
|
||||
cache.set(key, data, 600)
|
||||
|
||||
|
||||
class RelationMixin:
|
||||
m2m_field = None
|
||||
from_field = None
|
||||
to_field = None
|
||||
to_model = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
assert self.m2m_field is not None, '''
|
||||
`m2m_field` should not be `None`
|
||||
'''
|
||||
|
||||
self.from_field = self.m2m_field.m2m_field_name()
|
||||
self.to_field = self.m2m_field.m2m_reverse_field_name()
|
||||
self.to_model = self.m2m_field.related_model
|
||||
self.through = getattr(self.m2m_field.model, self.m2m_field.attname).through
|
||||
|
||||
def get_queryset(self):
|
||||
# 注意,此处拦截了 `get_queryset` 没有 `super`
|
||||
queryset = self.through.objects.all()
|
||||
return queryset
|
||||
|
||||
def send_m2m_changed_signal(self, instances, action):
|
||||
if not isinstance(instances, list):
|
||||
instances = [instances]
|
||||
|
||||
from_to_mapper = defaultdict(list)
|
||||
|
||||
for i in instances:
|
||||
to_id = getattr(i, self.to_field).id
|
||||
# TODO 优化,不应该每次都查询数据库
|
||||
from_obj = getattr(i, self.from_field)
|
||||
from_to_mapper[from_obj].append(to_id)
|
||||
|
||||
for from_obj, to_ids in from_to_mapper.items():
|
||||
m2m_changed.send(
|
||||
sender=self.through, instance=from_obj, action=action,
|
||||
reverse=False, model=self.to_model, pk_set=to_ids
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
self.send_m2m_changed_signal(instance, 'post_add')
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.delete()
|
||||
self.send_m2m_changed_signal(instance, 'post_remove')
|
||||
|
||||
|
||||
class QuerySetMixin:
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
serializer_class = self.get_serializer_class()
|
||||
|
||||
if serializer_class and hasattr(serializer_class, 'setup_eager_loading'):
|
||||
queryset = serializer_class.setup_eager_loading(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class AllowBulkDestroyMixin:
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
"""
|
||||
我们规定,批量删除的情况必须用 `id` 指定要删除的数据。
|
||||
"""
|
||||
query = str(filtered.query)
|
||||
return '`id` IN (' in query or '`id` =' in query
|
||||
|
||||
|
||||
class RoleAdminMixin:
|
||||
kwargs: dict
|
||||
user_id_url_kwarg = 'pk'
|
||||
|
||||
@lazyproperty
|
||||
def user(self):
|
||||
user_id = self.kwargs.get(self.user_id_url_kwarg)
|
||||
return UserModel.objects.get(id=user_id)
|
||||
|
||||
|
||||
class RoleUserMixin:
|
||||
request: Request
|
||||
|
||||
@lazyproperty
|
||||
def user(self):
|
||||
return self.request.user
|
|
@ -0,0 +1,7 @@
|
|||
from .common import *
|
||||
from .action import *
|
||||
from .patch import *
|
||||
from .filter import *
|
||||
from .permission import *
|
||||
from .queryset import *
|
||||
from .serializer import *
|
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from typing import Callable
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.const.http import POST
|
||||
from common.permissions import IsValidUser
|
||||
|
||||
|
||||
__all__ = ['SuggestionMixin', 'RenderToJsonMixin']
|
||||
|
||||
|
||||
class SuggestionMixin:
|
||||
suggestion_limit = 10
|
||||
|
||||
filter_queryset: Callable
|
||||
get_queryset: Callable
|
||||
paginate_queryset: Callable
|
||||
get_serializer: Callable
|
||||
get_paginated_response: Callable
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
|
||||
def suggestions(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
queryset = queryset[:self.suggestion_limit]
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RenderToJsonMixin:
|
||||
@action(methods=[POST], detail=False, url_path='render-to-json')
|
||||
def render_to_json(self, request: Request):
|
||||
data = {
|
||||
'title': (),
|
||||
'data': request.data,
|
||||
}
|
||||
|
||||
jms_context = getattr(request, 'jms_context', {})
|
||||
column_title_field_pairs = jms_context.get('column_title_field_pairs', ())
|
||||
data['title'] = column_title_field_pairs
|
||||
|
||||
if isinstance(request.data, (list, tuple)) and not any(request.data):
|
||||
error = _("Request file format may be wrong")
|
||||
return Response(data={"error": error}, status=400)
|
||||
return Response(data=data)
|
|
@ -0,0 +1,96 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from typing import Callable
|
||||
from rest_framework.response import Response
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.models.signals import m2m_changed
|
||||
|
||||
from .serializer import SerializerMixin
|
||||
from .filter import ExtraFilterFieldsMixin
|
||||
from .action import RenderToJsonMixin
|
||||
from .queryset import QuerySetMixin
|
||||
|
||||
|
||||
__all__ = [
|
||||
'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin', 'CommonMixin'
|
||||
]
|
||||
|
||||
|
||||
class PaginatedResponseMixin:
|
||||
paginate_queryset: Callable
|
||||
get_serializer: Callable
|
||||
get_paginated_response: Callable
|
||||
|
||||
def get_paginated_response_from_queryset(self, queryset):
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RelationMixin:
|
||||
m2m_field = None
|
||||
from_field = None
|
||||
to_field = None
|
||||
to_model = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
assert self.m2m_field is not None, '''
|
||||
`m2m_field` should not be `None`
|
||||
'''
|
||||
|
||||
self.from_field = self.m2m_field.m2m_field_name()
|
||||
self.to_field = self.m2m_field.m2m_reverse_field_name()
|
||||
self.to_model = self.m2m_field.related_model
|
||||
self.through = getattr(self.m2m_field.model, self.m2m_field.attname).through
|
||||
|
||||
def get_queryset(self):
|
||||
# 注意,此处拦截了 `get_queryset` 没有 `super`
|
||||
queryset = self.through.objects.all()
|
||||
return queryset
|
||||
|
||||
def send_m2m_changed_signal(self, instances, action):
|
||||
if not isinstance(instances, list):
|
||||
instances = [instances]
|
||||
|
||||
from_to_mapper = defaultdict(list)
|
||||
|
||||
for i in instances:
|
||||
to_id = getattr(i, self.to_field).id
|
||||
# TODO 优化,不应该每次都查询数据库
|
||||
from_obj = getattr(i, self.from_field)
|
||||
from_to_mapper[from_obj].append(to_id)
|
||||
|
||||
for from_obj, to_ids in from_to_mapper.items():
|
||||
m2m_changed.send(
|
||||
sender=self.through, instance=from_obj, action=action,
|
||||
reverse=False, model=self.to_model, pk_set=to_ids
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
self.send_m2m_changed_signal(instance, 'post_add')
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.delete()
|
||||
self.send_m2m_changed_signal(instance, 'post_remove')
|
||||
|
||||
|
||||
class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin, RenderToJsonMixin):
|
||||
pass
|
||||
|
||||
|
||||
class CommonMixin(SerializerMixin,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
RenderToJsonMixin):
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from itertools import chain
|
||||
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
|
||||
|
||||
|
||||
__all__ = ['ExtraFilterFieldsMixin']
|
||||
|
||||
|
||||
class ExtraFilterFieldsMixin:
|
||||
"""
|
||||
额外的 api filter
|
||||
"""
|
||||
default_added_filters = [CustomFilter, IDSpmFilter, IDInFilter]
|
||||
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
|
||||
extra_filter_fields = []
|
||||
extra_filter_backends = []
|
||||
|
||||
def get_filter_backends(self):
|
||||
if self.filter_backends != self.__class__.filter_backends:
|
||||
return self.filter_backends
|
||||
backends = list(chain(
|
||||
self.filter_backends,
|
||||
self.default_added_filters,
|
||||
self.extra_filter_backends
|
||||
))
|
||||
return backends
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
for backend in self.get_filter_backends():
|
||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||
return queryset
|
|
@ -0,0 +1,136 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from hashlib import md5
|
||||
from threading import Thread
|
||||
|
||||
from django.core.cache import cache
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.utils import lazyproperty
|
||||
|
||||
|
||||
__all__ = ['InterceptMixin', 'AsyncApiMixin']
|
||||
|
||||
|
||||
class InterceptMixin:
|
||||
"""
|
||||
Hack默认的dispatch, 让用户可以实现 self.do
|
||||
"""
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
request = self.initialize_request(request, *args, **kwargs)
|
||||
self.request = request
|
||||
self.headers = self.default_response_headers # deprecate?
|
||||
|
||||
try:
|
||||
self.initial(request, *args, **kwargs)
|
||||
|
||||
# Get the appropriate handler method
|
||||
if request.method.lower() in self.http_method_names:
|
||||
handler = getattr(self, request.method.lower(),
|
||||
self.http_method_not_allowed)
|
||||
else:
|
||||
handler = self.http_method_not_allowed
|
||||
|
||||
response = self.do(handler, request, *args, **kwargs)
|
||||
|
||||
except Exception as exc:
|
||||
response = self.handle_exception(exc)
|
||||
|
||||
self.response = self.finalize_response(request, response, *args, **kwargs)
|
||||
return self.response
|
||||
|
||||
|
||||
class AsyncApiMixin(InterceptMixin):
|
||||
def get_request_user_id(self):
|
||||
user = self.request.user
|
||||
if hasattr(user, 'id'):
|
||||
return str(user.id)
|
||||
return ''
|
||||
|
||||
@lazyproperty
|
||||
def async_cache_key(self):
|
||||
method = self.request.method
|
||||
path = self.get_request_md5()
|
||||
user = self.get_request_user_id()
|
||||
key = '{}_{}_{}'.format(method, path, user)
|
||||
return key
|
||||
|
||||
def get_request_md5(self):
|
||||
path = self.request.path
|
||||
query = {k: v for k, v in self.request.GET.items()}
|
||||
query.pop("_", None)
|
||||
query.pop('refresh', None)
|
||||
query = "&".join(["{}={}".format(k, v) for k, v in query.items()])
|
||||
full_path = "{}?{}".format(path, query)
|
||||
return md5(full_path.encode()).hexdigest()
|
||||
|
||||
@lazyproperty
|
||||
def initial_data(self):
|
||||
data = {
|
||||
"status": "running",
|
||||
"start_time": time.time(),
|
||||
"key": self.async_cache_key,
|
||||
}
|
||||
return data
|
||||
|
||||
def get_cache_data(self):
|
||||
key = self.async_cache_key
|
||||
if self.is_need_refresh():
|
||||
cache.delete(key)
|
||||
return None
|
||||
data = cache.get(key)
|
||||
return data
|
||||
|
||||
def do(self, handler, *args, **kwargs):
|
||||
if not self.is_need_async():
|
||||
return handler(*args, **kwargs)
|
||||
resp = self.do_async(handler, *args, **kwargs)
|
||||
return resp
|
||||
|
||||
def is_need_refresh(self):
|
||||
if self.request.GET.get("refresh"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_need_async(self):
|
||||
return False
|
||||
|
||||
def do_async(self, handler, *args, **kwargs):
|
||||
data = self.get_cache_data()
|
||||
if not data:
|
||||
t = Thread(
|
||||
target=self.do_in_thread,
|
||||
args=(handler, *args),
|
||||
kwargs=kwargs
|
||||
)
|
||||
t.start()
|
||||
resp = Response(self.initial_data)
|
||||
return resp
|
||||
status = data.get("status")
|
||||
resp = data.get("resp")
|
||||
if status == "ok" and resp:
|
||||
resp = Response(**resp)
|
||||
else:
|
||||
resp = Response(data)
|
||||
return resp
|
||||
|
||||
def do_in_thread(self, handler, *args, **kwargs):
|
||||
key = self.async_cache_key
|
||||
data = self.initial_data
|
||||
cache.set(key, data, 600)
|
||||
try:
|
||||
response = handler(*args, **kwargs)
|
||||
data["status"] = "ok"
|
||||
data["resp"] = {
|
||||
"data": response.data,
|
||||
"status": response.status_code
|
||||
}
|
||||
cache.set(key, data, 600)
|
||||
except Exception as e:
|
||||
data["error"] = str(e)
|
||||
data["status"] = "error"
|
||||
cache.set(key, data, 600)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.utils import lazyproperty
|
||||
|
||||
|
||||
__all__ = ['AllowBulkDestroyMixin', 'RoleAdminMixin', 'RoleUserMixin']
|
||||
|
||||
|
||||
class AllowBulkDestroyMixin:
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
"""
|
||||
我们规定,批量删除的情况必须用 `id` 指定要删除的数据。
|
||||
"""
|
||||
query = str(filtered.query)
|
||||
return '`id` IN (' in query or '`id` =' in query
|
||||
|
||||
|
||||
class RoleAdminMixin:
|
||||
kwargs: dict
|
||||
user_id_url_kwarg = 'pk'
|
||||
|
||||
@lazyproperty
|
||||
def user(self):
|
||||
user_id = self.kwargs.get(self.user_id_url_kwarg)
|
||||
user_model = get_user_model()
|
||||
return user_model.objects.get(id=user_id)
|
||||
|
||||
|
||||
class RoleUserMixin:
|
||||
request: Request
|
||||
|
||||
@lazyproperty
|
||||
def user(self):
|
||||
return self.request.user
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
__all__ = ['QuerySetMixin']
|
||||
|
||||
|
||||
class QuerySetMixin:
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
serializer_class = self.get_serializer_class()
|
||||
|
||||
if serializer_class and hasattr(serializer_class, 'setup_eager_loading'):
|
||||
queryset = serializer_class.setup_eager_loading(queryset)
|
||||
return queryset
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework.request import Request
|
||||
|
||||
__all__ = ['SerializerMixin']
|
||||
|
||||
|
||||
class SerializerMixin:
|
||||
""" 根据用户请求动作的不同,获取不同的 `serializer_class `"""
|
||||
|
||||
action: str
|
||||
request: Request
|
||||
|
||||
serializer_classes = None
|
||||
single_actions = ['put', 'retrieve', 'patch']
|
||||
|
||||
def get_serializer_class_by_view_action(self):
|
||||
if not hasattr(self, 'serializer_classes'):
|
||||
return None
|
||||
if not isinstance(self.serializer_classes, dict):
|
||||
return None
|
||||
|
||||
view_action = self.request.query_params.get('action') or self.action or 'list'
|
||||
serializer_class = self.serializer_classes.get(view_action)
|
||||
|
||||
if serializer_class is None:
|
||||
view_method = self.request.method.lower()
|
||||
serializer_class = self.serializer_classes.get(view_method)
|
||||
|
||||
if serializer_class is None and view_action in self.single_actions:
|
||||
serializer_class = self.serializer_classes.get('single')
|
||||
if serializer_class is None:
|
||||
serializer_class = self.serializer_classes.get('display')
|
||||
if serializer_class is None:
|
||||
serializer_class = self.serializer_classes.get('default')
|
||||
return serializer_class
|
||||
|
||||
def get_serializer_class(self):
|
||||
serializer_class = self.get_serializer_class_by_view_action()
|
||||
if serializer_class is None:
|
||||
serializer_class = super().get_serializer_class()
|
||||
return serializer_class
|
|
@ -1,49 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# coding: utf-8
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.utils import timezone
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
|
||||
__all__ = ["DatetimeSearchMixin", "PermissionsMixin"]
|
||||
from rest_framework.request import Request
|
||||
|
||||
|
||||
class DatetimeSearchMixin:
|
||||
date_format = '%Y-%m-%d'
|
||||
date_from = date_to = None
|
||||
|
||||
def get_date_range(self):
|
||||
date_from_s = self.request.GET.get('date_from')
|
||||
date_to_s = self.request.GET.get('date_to')
|
||||
|
||||
if date_from_s:
|
||||
date_from = timezone.datetime.strptime(date_from_s, self.date_format)
|
||||
tz = timezone.get_current_timezone()
|
||||
self.date_from = tz.localize(date_from)
|
||||
else:
|
||||
self.date_from = timezone.now() - timezone.timedelta(7)
|
||||
|
||||
if date_to_s:
|
||||
date_to = timezone.datetime.strptime(
|
||||
date_to_s + ' 23:59:59', self.date_format + ' %H:%M:%S'
|
||||
)
|
||||
self.date_to = date_to.replace(
|
||||
tzinfo=timezone.get_current_timezone()
|
||||
)
|
||||
else:
|
||||
self.date_to = timezone.now()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.get_date_range()
|
||||
return super().get(request, *args, **kwargs)
|
||||
__all__ = ["PermissionsMixin"]
|
||||
|
||||
|
||||
class PermissionsMixin(UserPassesTestMixin):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
request: Request
|
||||
|
||||
def get_permissions(self):
|
||||
return self.permission_classes
|
||||
|
@ -56,17 +23,3 @@ class PermissionsMixin(UserPassesTestMixin):
|
|||
return True
|
||||
|
||||
|
||||
class SuggestionMixin:
|
||||
suggestion_mini_count = 10
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
|
||||
def suggestions(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
queryset = queryset[:self.suggestion_mini_count]
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
|
|
@ -3,8 +3,8 @@ import hmac
|
|||
import base64
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.message.backends.utils import digest, as_request
|
||||
from common.message.backends.mixin import BaseRequest
|
||||
from common.sdk.im.utils import digest, as_request
|
||||
from common.sdk.im.mixin import BaseRequest
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
|
@ -4,8 +4,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from rest_framework.exceptions import APIException
|
||||
|
||||
from common.utils.common import get_logger
|
||||
from common.message.backends.utils import digest
|
||||
from common.message.backends.mixin import RequestMixin, BaseRequest
|
||||
from common.sdk.im.utils import digest
|
||||
from common.sdk.im.mixin import RequestMixin, BaseRequest
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
@ -6,7 +6,7 @@ from django.core.cache import cache
|
|||
from .utils import DictWrapper
|
||||
from common.utils.common import get_logger
|
||||
from common.utils import lazyproperty
|
||||
from common.message.backends.utils import set_default, as_request
|
||||
from common.sdk.im.utils import set_default, as_request
|
||||
|
||||
from . import exceptions as exce
|
||||
|
|
@ -3,7 +3,7 @@ import inspect
|
|||
from inspect import Parameter
|
||||
|
||||
from common.utils.common import get_logger
|
||||
from common.message.backends import exceptions as exce
|
||||
from common.sdk.im import exceptions as exce
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
@ -4,8 +4,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from rest_framework.exceptions import APIException
|
||||
|
||||
from common.utils.common import get_logger
|
||||
from common.message.backends.utils import digest, DictWrapper, update_values, set_default
|
||||
from common.message.backends.mixin import RequestMixin, BaseRequest
|
||||
from common.sdk.im.utils import digest, DictWrapper, update_values, set_default
|
||||
from common.sdk.im.mixin import RequestMixin, BaseRequest
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
@ -10,7 +10,11 @@ def contains_time_period(time_periods):
|
|||
|
||||
current_time = local_now().strftime('%H:%M')
|
||||
today_time_period = next(filter(lambda x: str(x['id']) == local_now().strftime("%w"), time_periods))
|
||||
for time in today_time_period['value'].split('、'):
|
||||
today_time_period = today_time_period['value']
|
||||
if not today_time_period:
|
||||
return False
|
||||
|
||||
for time in today_time_period.split('、'):
|
||||
start, end = time.split('~')
|
||||
end = "24:00" if end == "00:00" else end
|
||||
if start <= current_time <= end:
|
||||
|
|
|
@ -28,6 +28,10 @@ def local_now():
|
|||
return as_current_tz(utc_now())
|
||||
|
||||
|
||||
def local_now_display(fmt='%Y-%m-%d %H:%M:%S'):
|
||||
return local_now().strftime(fmt)
|
||||
|
||||
|
||||
_rest_dt_field = DateTimeField()
|
||||
dt_parser = _rest_dt_field.to_internal_value
|
||||
dt_formatter = _rest_dt_field.to_representation
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.http.response import JsonResponse, HttpResponse
|
|||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import AllowAny
|
||||
from collections import Counter
|
||||
from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.models import User
|
||||
|
|
|
@ -4,27 +4,39 @@ from django.templatetags.static import static
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
default_context = {
|
||||
'DEFAULT_PK': '00000000-0000-0000-0000-000000000000',
|
||||
'LOGO_URL': static('img/logo.png'),
|
||||
'LOGO_TEXT_URL': static('img/logo_text.png'),
|
||||
'LOGIN_IMAGE_URL': static('img/login_image.jpg'),
|
||||
'FAVICON_URL': static('img/facio.ico'),
|
||||
'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'),
|
||||
'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_logo.png'),
|
||||
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_logo.png'),
|
||||
'LOGIN_FEISHU_LOGO_URL': static('img/login_feishu_logo.png'),
|
||||
'JMS_TITLE': _('JumpServer Open Source Bastion Host'),
|
||||
}
|
||||
|
||||
default_interface = {
|
||||
'login_title': default_context['JMS_TITLE'],
|
||||
'logo_logout': default_context['LOGO_URL'],
|
||||
'logo_index': default_context['LOGO_TEXT_URL'],
|
||||
'login_image': default_context['LOGIN_IMAGE_URL'],
|
||||
'favicon': default_context['FAVICON_URL'],
|
||||
}
|
||||
|
||||
|
||||
def jumpserver_processor(request):
|
||||
# Setting default pk
|
||||
context = {
|
||||
'DEFAULT_PK': '00000000-0000-0000-0000-000000000000',
|
||||
'LOGO_URL': static('img/logo.png'),
|
||||
'LOGO_TEXT_URL': static('img/logo_text.png'),
|
||||
'LOGIN_IMAGE_URL': static('img/login_image.jpg'),
|
||||
'FAVICON_URL': static('img/facio.ico'),
|
||||
'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'),
|
||||
'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_logo.png'),
|
||||
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_logo.png'),
|
||||
'LOGIN_FEISHU_LOGO_URL': static('img/login_feishu_logo.png'),
|
||||
'JMS_TITLE': _('JumpServer Open Source Bastion Host'),
|
||||
context = default_context
|
||||
context.update({
|
||||
'VERSION': settings.VERSION,
|
||||
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021',
|
||||
'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION,
|
||||
'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL,
|
||||
'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME,
|
||||
'SECURITY_VIEW_AUTH_NEED_MFA': settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
||||
}
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:39e9d8c61c6986a067d9c0c82d2a459e07bcc78d07d9bdd1c5934154ea3a198d
|
||||
size 91321
|
||||
oid sha256:55a2062981ea7eef4ca28142f325f52e15cb7679ad0a2600234a5bdb6d005c87
|
||||
size 89996
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,7 +12,10 @@ from notifications.serializers import (
|
|||
UserMsgSubscriptionSerializer,
|
||||
)
|
||||
|
||||
__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet', 'UserMsgSubscriptionViewSet')
|
||||
__all__ = (
|
||||
'BackendListView', 'SystemMsgSubscriptionViewSet',
|
||||
'UserMsgSubscriptionViewSet', 'get_all_test_messages'
|
||||
)
|
||||
|
||||
|
||||
class BackendListView(APIView):
|
||||
|
@ -80,3 +83,43 @@ class UserMsgSubscriptionViewSet(ListModelMixin,
|
|||
queryset = UserMsgSubscription.objects.all()
|
||||
serializer_class = UserMsgSubscriptionSerializer
|
||||
permission_classes = (IsObjectOwner | IsSuperUser, OnlySuperUserCanList)
|
||||
|
||||
|
||||
def get_all_test_messages(request):
|
||||
import textwrap
|
||||
from ..notifications import Message
|
||||
from django.shortcuts import HttpResponse
|
||||
|
||||
msgs_cls = Message.get_all_sub_messages()
|
||||
html_data = '<h3>HTML 格式 </h3>'
|
||||
text_data = '<h3>Text 格式</h3>'
|
||||
|
||||
for msg_cls in msgs_cls:
|
||||
try:
|
||||
msg = msg_cls.gen_test_msg()
|
||||
if not msg:
|
||||
continue
|
||||
msg_html = msg.html_msg_with_sign['message']
|
||||
msg_text = msg.text_msg_with_sign['message']
|
||||
except NotImplementedError:
|
||||
msg_html = msg_text = '没有实现方法'
|
||||
except Exception as e:
|
||||
msg_html = msg_text = 'Error: ' + str(e)
|
||||
|
||||
html_data += """
|
||||
<h3>{}</h3>
|
||||
{}
|
||||
<hr />
|
||||
""".format(msg_cls.__name__, msg_html)
|
||||
|
||||
text_data += textwrap.dedent("""
|
||||
<h3>{}</h3>
|
||||
<pre>
|
||||
{}
|
||||
</pre>
|
||||
<br/>
|
||||
<hr />
|
||||
""").format(msg_cls.__name__, msg_text)
|
||||
return HttpResponse(html_data + text_data)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.conf import settings
|
||||
from common.message.backends.dingtalk import DingTalk as Client
|
||||
from common.sdk.im.dingtalk import DingTalk as Client
|
||||
from .base import BackendBase
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf import settings
|
||||
|
||||
from common.message.backends.feishu import FeiShu as Client
|
||||
from common.sdk.im.feishu import FeiShu as Client
|
||||
from .base import BackendBase
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf import settings
|
||||
|
||||
from common.message.backends.sms.alibaba import AlibabaSMS as Client
|
||||
from common.sdk.sms.alibaba import AlibabaSMS as Client
|
||||
from .base import BackendBase
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf import settings
|
||||
|
||||
from common.message.backends.wecom import WeCom as Client
|
||||
from common.sdk.im.wecom import WeCom as Client
|
||||
from .base import BackendBase
|
||||
|
||||
|
||||
|
|
|
@ -2,22 +2,23 @@ import traceback
|
|||
from html2text import HTML2Text
|
||||
from typing import Iterable
|
||||
from itertools import chain
|
||||
import textwrap
|
||||
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils.timezone import local_now
|
||||
from common.utils import lazyproperty
|
||||
from settings.utils import get_login_title
|
||||
from users.models import User
|
||||
from notifications.backends import BACKEND
|
||||
from .models import SystemMsgSubscription, UserMsgSubscription
|
||||
|
||||
__all__ = ('SystemMessage', 'UserMessage', 'system_msgs')
|
||||
__all__ = ('SystemMessage', 'UserMessage', 'system_msgs', 'Message')
|
||||
|
||||
|
||||
system_msgs = []
|
||||
user_msgs = []
|
||||
all_msgs = []
|
||||
|
||||
|
||||
class MessageType(type):
|
||||
|
@ -55,7 +56,6 @@ class Message(metaclass=MessageType):
|
|||
- publish 该方法的实现与消息订阅的表结构有关
|
||||
- send_msg
|
||||
"""
|
||||
|
||||
message_type_label: str
|
||||
category: str
|
||||
category_label: str
|
||||
|
@ -84,16 +84,13 @@ class Message(metaclass=MessageType):
|
|||
backend = BACKEND(backend)
|
||||
if not backend.is_enable:
|
||||
continue
|
||||
|
||||
get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
|
||||
try:
|
||||
msg = get_msg_method()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
|
||||
msg = get_msg_method()
|
||||
client = backend.client()
|
||||
client.send_msg(users, **msg)
|
||||
except Exception:
|
||||
except NotImplementedError:
|
||||
continue
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
@classmethod
|
||||
|
@ -111,10 +108,7 @@ class Message(metaclass=MessageType):
|
|||
|
||||
@staticmethod
|
||||
def get_common_msg() -> dict:
|
||||
return {
|
||||
'subject': '',
|
||||
'message': ''
|
||||
}
|
||||
return {'subject': '', 'message': ''}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
return self.get_common_msg()
|
||||
|
@ -133,11 +127,41 @@ class Message(metaclass=MessageType):
|
|||
|
||||
@lazyproperty
|
||||
def text_msg(self) -> dict:
|
||||
return self.get_text_msg()
|
||||
msg = self.get_text_msg()
|
||||
return msg
|
||||
|
||||
@lazyproperty
|
||||
def html_msg(self) -> dict:
|
||||
return self.get_html_msg()
|
||||
msg = self.get_html_msg()
|
||||
return msg
|
||||
|
||||
@lazyproperty
|
||||
def html_msg_with_sign(self):
|
||||
msg = self.get_html_msg()
|
||||
msg['message'] = textwrap.dedent("""
|
||||
{}
|
||||
<small>
|
||||
<br />
|
||||
—
|
||||
<br />
|
||||
{}
|
||||
</small>
|
||||
""").format(msg['message'], self.signature)
|
||||
return msg
|
||||
|
||||
@lazyproperty
|
||||
def text_msg_with_sign(self):
|
||||
msg = self.get_text_msg()
|
||||
msg['message'] = textwrap.dedent("""
|
||||
{}
|
||||
—
|
||||
{}
|
||||
""").format(msg['message'], self.signature)
|
||||
return msg
|
||||
|
||||
@lazyproperty
|
||||
def signature(self):
|
||||
return get_login_title()
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 支持不同发送消息的方式定义自己的消息内容,比如有些支持 html 标签
|
||||
|
@ -159,16 +183,16 @@ class Message(metaclass=MessageType):
|
|||
return self.text_msg
|
||||
|
||||
def get_email_msg(self) -> dict:
|
||||
return self.html_msg
|
||||
return self.html_msg_with_sign
|
||||
|
||||
def get_site_msg_msg(self) -> dict:
|
||||
return self.html_msg
|
||||
|
||||
def get_sms_msg(self) -> dict:
|
||||
return self.text_msg
|
||||
return self.text_msg_with_sign
|
||||
|
||||
@classmethod
|
||||
def test_all_messages(cls):
|
||||
def get_all_sub_messages(cls):
|
||||
def get_subclasses(cls):
|
||||
"""returns all subclasses of argument, cls"""
|
||||
if issubclass(cls, type):
|
||||
|
@ -180,6 +204,12 @@ class Message(metaclass=MessageType):
|
|||
return subclasses
|
||||
|
||||
messages_cls = get_subclasses(cls)
|
||||
return messages_cls
|
||||
|
||||
@classmethod
|
||||
def test_all_messages(cls):
|
||||
messages_cls = cls.get_all_sub_messages()
|
||||
|
||||
for _cls in messages_cls:
|
||||
try:
|
||||
msg = _cls.send_test_msg()
|
||||
|
@ -225,6 +255,11 @@ class UserMessage(Message):
|
|||
sub = UserMsgSubscription.objects.get(user=self.user)
|
||||
self.send_msg([self.user], sub.receive_backends)
|
||||
|
||||
@classmethod
|
||||
def get_test_user(cls):
|
||||
from users.models import User
|
||||
return User.objects.all().first()
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
from django.urls import path
|
||||
from django.conf import settings
|
||||
|
||||
from notifications import api
|
||||
|
||||
|
@ -14,3 +15,8 @@ router.register('site-message', api.SiteMessageViewSet, 'site-message')
|
|||
urlpatterns = [
|
||||
path('backends/', api.BackendListView.as_view(), name='backends')
|
||||
] + router.urls
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path('debug-msgs/', api.get_all_test_messages, name='debug-all-msgs')
|
||||
]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from notifications.notifications import SystemMessage
|
||||
from notifications.models import SystemMsgSubscription
|
||||
|
@ -14,14 +15,18 @@ class ServerPerformanceMessage(SystemMessage):
|
|||
category_label = _('Operations')
|
||||
message_type_label = _('Server performance')
|
||||
|
||||
def __init__(self, msg):
|
||||
self._msg = msg
|
||||
def __init__(self, terms_with_errors):
|
||||
self.terms_with_errors = terms_with_errors
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = self._msg[:80]
|
||||
subject = _("Terminal health check warning")
|
||||
context = {
|
||||
'terms_with_errors': self.terms_with_errors
|
||||
}
|
||||
message = render_to_string('ops/_msg_terminal_performance.html', context)
|
||||
return {
|
||||
'subject': subject.replace('<br>', '; '),
|
||||
'message': self._msg
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -33,17 +38,21 @@ class ServerPerformanceMessage(SystemMessage):
|
|||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
alarm_messages = []
|
||||
from terminal.models import Terminal
|
||||
items_mapper = ServerPerformanceCheckUtil.items_mapper
|
||||
for item, data in items_mapper.items():
|
||||
msg = data['alarm_msg_format']
|
||||
max_threshold = data['max_threshold']
|
||||
value = 123
|
||||
msg = msg.format(max_threshold=max_threshold, value=value, name='Fake terminal')
|
||||
alarm_messages.append(msg)
|
||||
terms_with_errors = []
|
||||
terms = Terminal.objects.all()[:5]
|
||||
|
||||
msg = '<br>'.join(alarm_messages)
|
||||
return cls(msg)
|
||||
for i, term in enumerate(terms, 1):
|
||||
errors = []
|
||||
for item, data in items_mapper.items():
|
||||
msg = data['alarm_msg_format']
|
||||
max_threshold = data['max_threshold']
|
||||
value = 123 // i+1
|
||||
msg = msg.format(max_threshold=max_threshold, value=value, name=term.name)
|
||||
errors.append(msg)
|
||||
terms_with_errors.append([term, errors])
|
||||
return cls(terms_with_errors)
|
||||
|
||||
|
||||
class ServerPerformanceCheckUtil(object):
|
||||
|
@ -56,59 +65,65 @@ class ServerPerformanceCheckUtil(object):
|
|||
'disk_used': {
|
||||
'default': 0,
|
||||
'max_threshold': 80,
|
||||
'alarm_msg_format': _(
|
||||
'Disk used more than {max_threshold}%: => {value} ({name})'
|
||||
)
|
||||
'alarm_msg_format': _('Disk used more than {max_threshold}%: => {value}')
|
||||
},
|
||||
'memory_used': {
|
||||
'default': 0,
|
||||
'max_threshold': 85,
|
||||
'alarm_msg_format': _(
|
||||
'Memory used more than {max_threshold}%: => {value} ({name})'
|
||||
),
|
||||
'alarm_msg_format': _('Memory used more than {max_threshold}%: => {value}'),
|
||||
},
|
||||
'cpu_load': {
|
||||
'default': 0,
|
||||
'max_threshold': 5,
|
||||
'alarm_msg_format': _(
|
||||
'CPU load more than {max_threshold}: => {value} ({name})'
|
||||
),
|
||||
'alarm_msg_format': _('CPU load more than {max_threshold}: => {value}'),
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.alarm_messages = []
|
||||
self.terms_with_errors = []
|
||||
self._terminals = []
|
||||
self._terminal = None
|
||||
|
||||
def check_and_publish(self):
|
||||
self.check()
|
||||
self.publish()
|
||||
|
||||
def check(self):
|
||||
self.alarm_messages = []
|
||||
self.terms_with_errors = []
|
||||
self.initial_terminals()
|
||||
for item, data in self.items_mapper.items():
|
||||
for self._terminal in self._terminals:
|
||||
self.check_item(item, data)
|
||||
|
||||
def check_item(self, item, data):
|
||||
for term in self._terminals:
|
||||
errors = self.check_terminal(term)
|
||||
if not errors:
|
||||
continue
|
||||
self.terms_with_errors.append((term, errors))
|
||||
|
||||
def check_terminal(self, term):
|
||||
errors = []
|
||||
for item, data in self.items_mapper.items():
|
||||
error = self.check_item(term, item, data)
|
||||
if not error:
|
||||
continue
|
||||
errors.append(error)
|
||||
return errors
|
||||
|
||||
@staticmethod
|
||||
def check_item(term, item, data):
|
||||
default = data['default']
|
||||
max_threshold = data['max_threshold']
|
||||
value = getattr(self._terminal.stat, item, default)
|
||||
value = getattr(term.stat, item, default)
|
||||
|
||||
if isinstance(value, bool) and value != max_threshold:
|
||||
return
|
||||
elif isinstance(value, (int, float)) and value < max_threshold:
|
||||
return
|
||||
msg = data['alarm_msg_format']
|
||||
msg = msg.format(max_threshold=max_threshold, value=value, name=self._terminal.name)
|
||||
self.alarm_messages.append(msg)
|
||||
error = msg.format(max_threshold=max_threshold, value=value, name=term.name)
|
||||
return error
|
||||
|
||||
def publish(self):
|
||||
if not self.alarm_messages:
|
||||
if not self.terms_with_errors:
|
||||
return
|
||||
msg = '<br>'.join(self.alarm_messages)
|
||||
ServerPerformanceMessage(msg).publish()
|
||||
ServerPerformanceMessage(self.terms_with_errors).publish()
|
||||
|
||||
def initial_terminals(self):
|
||||
terminals = []
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<div>
|
||||
{% for term, errors in terms_with_errors %}
|
||||
<h4>{{ term.name }}</h4>
|
||||
<ul>
|
||||
{% for error in errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -48,6 +48,8 @@ class OrgViewSet(BulkModelViewSet):
|
|||
queryset = Organization.objects.all()
|
||||
serializer_class = OrgSerializer
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
def get_serializer_class(self):
|
||||
mapper = {
|
||||
|
@ -74,7 +76,7 @@ class OrgViewSet(BulkModelViewSet):
|
|||
|
||||
def perform_destroy(self, instance):
|
||||
if str(current_org) == str(instance):
|
||||
msg = _('The current organization ({}) cannot be deleted'.format(current_org))
|
||||
msg = _('The current organization ({}) cannot be deleted').format(current_org)
|
||||
raise PermissionDenied(detail=msg)
|
||||
|
||||
for model in org_related_models:
|
||||
|
|
|
@ -22,6 +22,8 @@ class ApplicationPermissionViewSet(BasePermissionViewSet):
|
|||
custom_filter_fields = BasePermissionViewSet.custom_filter_fields + [
|
||||
'application_id', 'application', 'app', 'app_name'
|
||||
]
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().prefetch_related(
|
||||
|
|
|
@ -21,3 +21,5 @@ class AssetPermissionViewSet(OrgBulkModelViewSet):
|
|||
serializer_class = serializers.AssetPermissionSerializer
|
||||
filterset_class = AssetPermissionFilter
|
||||
search_fields = ('name',)
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
|
@ -6,13 +8,7 @@ from common.utils import reverse as js_reverse
|
|||
from notifications.notifications import UserMessage
|
||||
|
||||
|
||||
class BasePermMsg(UserMessage):
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
return
|
||||
|
||||
|
||||
class PermedWillExpireUserMsg(BasePermMsg):
|
||||
class PermedAssetsWillExpireUserMsg(UserMessage):
|
||||
def __init__(self, user, assets):
|
||||
super().__init__(user)
|
||||
self.assets = assets
|
||||
|
@ -40,7 +36,7 @@ class PermedWillExpireUserMsg(BasePermMsg):
|
|||
return cls(user, assets)
|
||||
|
||||
|
||||
class AssetPermsWillExpireForOrgAdminMsg(BasePermMsg):
|
||||
class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
|
||||
def __init__(self, user, perms, org):
|
||||
super().__init__(user)
|
||||
|
@ -84,7 +80,7 @@ class AssetPermsWillExpireForOrgAdminMsg(BasePermMsg):
|
|||
return cls(user, perms, org)
|
||||
|
||||
|
||||
class PermedAppsWillExpireUserMsg(BasePermMsg):
|
||||
class PermedAppsWillExpireUserMsg(UserMessage):
|
||||
def __init__(self, user, apps):
|
||||
super().__init__(user)
|
||||
self.apps = apps
|
||||
|
@ -112,7 +108,7 @@ class PermedAppsWillExpireUserMsg(BasePermMsg):
|
|||
return cls(user, apps)
|
||||
|
||||
|
||||
class AppPermsWillExpireForOrgAdminMsg(BasePermMsg):
|
||||
class AppPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
def __init__(self, user, perms, org):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
|
@ -120,12 +116,9 @@ class AppPermsWillExpireForOrgAdminMsg(BasePermMsg):
|
|||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
perm_detail_url = urljoin(settings.SITE_URL, '/ui/#/perms/app-permissions/{}')
|
||||
for perm in self.perms:
|
||||
url = js_reverse(
|
||||
'perms:application-permission-detail',
|
||||
kwargs={'pk': perm.id}, external=True,
|
||||
api_to_ui=True
|
||||
) + f'?oid={perm.org_id}'
|
||||
url = perm_detail_url.format(perm.id) + f'?oid={perm.org_id}'
|
||||
items_with_url.append([perm.name, url])
|
||||
return items_with_url
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ from common.utils import get_logger
|
|||
from common.utils.timezone import local_now, dt_formatter, dt_parser
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from perms.notifications import (
|
||||
PermedWillExpireUserMsg, AssetPermsWillExpireForOrgAdminMsg,
|
||||
PermedAssetsWillExpireUserMsg, AssetPermsWillExpireForOrgAdminMsg,
|
||||
PermedAppsWillExpireUserMsg, AppPermsWillExpireForOrgAdminMsg
|
||||
)
|
||||
from perms.models import AssetPermission, ApplicationPermission
|
||||
|
@ -83,7 +83,7 @@ def check_asset_permission_will_expired():
|
|||
user_asset_mapper[u].update(assets)
|
||||
|
||||
for user, assets in user_asset_mapper.items():
|
||||
PermedWillExpireUserMsg(user, assets).publish_async()
|
||||
PermedAssetsWillExpireUserMsg(user, assets).publish_async()
|
||||
|
||||
for org, perms in org_perm_mapper.items():
|
||||
org_admins = org.admins.all()
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
|
||||
<ul>
|
||||
{% for item, url in items_with_url %}
|
||||
<li><a href="{{ url }}">{{ item }}</a></li>
|
||||
<li><a href="{{ url }}" target="_blank">{{ item }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
@ -17,8 +17,5 @@
|
|||
|
||||
<br />
|
||||
<p>
|
||||
---<br />
|
||||
<small>
|
||||
{% trans 'If you have any question, please contact the administrator' %}
|
||||
</small>
|
||||
{% trans 'If you have any question, please contact the administrator' %}
|
||||
</p>
|
||||
|
|
|
@ -4,7 +4,7 @@ from rest_framework.exceptions import APIException
|
|||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms.alibaba import AlibabaSMS
|
||||
from common.sdk.sms.alibaba import AlibabaSMS
|
||||
from settings.models import Setting
|
||||
from common.permissions import IsSuperUser
|
||||
from common.exceptions import JMSException
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from django.conf import settings
|
||||
from common.permissions import IsSuperUser
|
||||
from common.message.backends.dingtalk import DingTalk
|
||||
from common.sdk.im.dingtalk import DingTalk
|
||||
|
||||
from .. import serializers
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from settings.models import Setting
|
||||
from common.permissions import IsSuperUser
|
||||
from common.message.backends.feishu import FeiShu
|
||||
from common.sdk.im.feishu import FeiShu
|
||||
|
||||
from .. import serializers
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework.permissions import AllowAny
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.templatetags.static import static
|
||||
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
from common.utils import get_logger
|
||||
from .. import serializers
|
||||
from ..utils import get_interface_setting
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -19,30 +18,14 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
|||
|
||||
@staticmethod
|
||||
def get_logo_urls():
|
||||
logo_urls = {
|
||||
'logo_logout': static('img/logo.png'),
|
||||
'logo_index': static('img/logo_text.png'),
|
||||
'login_image': static('img/login_image.jpg'),
|
||||
'favicon': static('img/facio.ico')
|
||||
}
|
||||
if not settings.XPACK_ENABLED:
|
||||
return logo_urls
|
||||
from xpack.plugins.interface.models import Interface
|
||||
obj = Interface.interface()
|
||||
if not obj:
|
||||
return logo_urls
|
||||
for attr in ['logo_logout', 'logo_index', 'login_image', 'favicon']:
|
||||
if getattr(obj, attr, '') and getattr(obj, attr).url:
|
||||
logo_urls.update({attr: getattr(obj, attr).url})
|
||||
return logo_urls
|
||||
interface = get_interface_setting()
|
||||
keys = ['logo_logout', 'logo_index', 'login_image', 'favicon']
|
||||
return {k: interface[k] for k in keys}
|
||||
|
||||
@staticmethod
|
||||
def get_login_title():
|
||||
default_title = _('Welcome to the JumpServer open source Bastion Host')
|
||||
if not settings.XPACK_ENABLED:
|
||||
return default_title
|
||||
from xpack.plugins.interface.models import Interface
|
||||
return Interface.get_login_title()
|
||||
interface = get_interface_setting()
|
||||
return interface['login_title']
|
||||
|
||||
def get_object(self):
|
||||
instance = {
|
||||
|
|
|
@ -2,7 +2,7 @@ from rest_framework.generics import ListAPIView
|
|||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsSuperUser
|
||||
from common.message.backends.sms import BACKENDS
|
||||
from common.sdk.sms import BACKENDS
|
||||
from settings.serializers.sms import SMSBackendSerializer
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from rest_framework.exceptions import APIException
|
|||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms.tencent import TencentSMS
|
||||
from common.sdk.sms.tencent import TencentSMS
|
||||
from settings.models import Setting
|
||||
from common.permissions import IsSuperUser
|
||||
from common.exceptions import JMSException
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from settings.models import Setting
|
||||
from common.permissions import IsSuperUser
|
||||
from common.message.backends.wecom import WeCom
|
||||
from common.sdk.im.wecom import WeCom
|
||||
|
||||
from .. import serializers
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.message.backends.sms import BACKENDS
|
||||
from common.sdk.sms import BACKENDS
|
||||
|
||||
__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
# coding: utf-8
|
||||
from jumpserver.context_processor import default_interface
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class ObjectDict(dict):
|
||||
|
@ -16,3 +18,14 @@ class ObjectDict(dict):
|
|||
del self[name]
|
||||
else:
|
||||
raise AttributeError("No such attribute: " + name)
|
||||
|
||||
|
||||
def get_interface_setting():
|
||||
if not settings.XPACK_ENABLED:
|
||||
return default_interface
|
||||
from xpack.plugins.interface.models import Interface
|
||||
return Interface.get_interface_setting()
|
||||
|
||||
|
||||
def get_login_title():
|
||||
return get_interface_setting()['login_title']
|
||||
|
|
|
@ -17,6 +17,7 @@ logger = logging.getLogger(__file__)
|
|||
class TaskViewSet(BulkModelViewSet):
|
||||
queryset = Task.objects.all()
|
||||
serializer_class = serializers.TaskSerializer
|
||||
filterset_fields = ('is_finished',)
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
|
||||
|
|
|
@ -134,11 +134,8 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model):
|
|||
|
||||
@staticmethod
|
||||
def get_login_title_setting():
|
||||
login_title = None
|
||||
if settings.XPACK_ENABLED:
|
||||
from xpack.plugins.interface.models import Interface
|
||||
login_title = Interface.get_login_title()
|
||||
return {'TERMINAL_HEADER_TITLE': login_title}
|
||||
from settings.utils import get_login_title
|
||||
return {'TERMINAL_HEADER_TITLE': get_login_title()}
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
|
|
|
@ -13,6 +13,8 @@ from notifications.models import SystemMsgSubscription
|
|||
from notifications.backends import BACKEND
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.utils import lazyproperty
|
||||
from common.utils.timezone import local_now_display
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -73,25 +75,26 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage):
|
|||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
command = Command.objects.first().to_dict()
|
||||
command['session'] = Session.objects.first().id
|
||||
return cls(command)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
command = self.command
|
||||
|
||||
with tmp_to_root_org():
|
||||
session = Session.objects.get(id=command['session'])
|
||||
session_detail_url = reverse(
|
||||
'api-terminal:session-detail', kwargs={'pk': command['session']},
|
||||
external=True, api_to_ui=True
|
||||
)
|
||||
) + '?oid={}'.format(self.command['org_id'])
|
||||
level = Command.get_risk_level_str(command['risk_level'])
|
||||
items = {
|
||||
_("Asset"): command['asset'],
|
||||
_("User"): command['user'],
|
||||
_("Level"): level,
|
||||
_("Date"): local_now_display(),
|
||||
}
|
||||
context = {
|
||||
'command': command['input'],
|
||||
'hostname': command['asset'],
|
||||
'host_ip': session.asset_obj.ip,
|
||||
'user': command['user'],
|
||||
'risk_level': Command.get_risk_level_str(command['risk_level']),
|
||||
'session_detail_url': session_detail_url,
|
||||
'oid': session.org_id
|
||||
'items': items,
|
||||
'session_url': session_detail_url,
|
||||
"command": command['input'],
|
||||
}
|
||||
message = render_to_string('terminal/_msg_command_alert.html', context)
|
||||
return {
|
||||
|
@ -122,19 +125,25 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
|
|||
|
||||
def get_html_msg(self) -> dict:
|
||||
command = self.command
|
||||
_input = command['input']
|
||||
_input = _input.replace('\n', '<br>')
|
||||
|
||||
assets_with_url = []
|
||||
for asset in command['assets']:
|
||||
url = reverse('assets:asset-detail', kwargs={'pk': asset.id}, api_to_ui=True, external=True)
|
||||
url = reverse(
|
||||
'assets:asset-detail', kwargs={'pk': asset.id},
|
||||
api_to_ui=True, external=True
|
||||
) + '?oid={}'.format(asset.org_id)
|
||||
assets_with_url.append([asset, url])
|
||||
|
||||
level = Command.get_risk_level_str(command['risk_level'])
|
||||
items = {
|
||||
_("User"): command['user'],
|
||||
_("Level"): level,
|
||||
_("Date"): local_now_display(),
|
||||
}
|
||||
|
||||
context = {
|
||||
'command': _input,
|
||||
'items': items,
|
||||
'assets_with_url': assets_with_url,
|
||||
'user': command['user'],
|
||||
'risk_level': Command.get_risk_level_str(command['risk_level'])
|
||||
'command': command['input'],
|
||||
}
|
||||
message = render_to_string('terminal/_msg_command_execute_alert.html', context)
|
||||
return {
|
||||
|
|
|
@ -42,13 +42,10 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
|
|||
|
||||
|
||||
class SessionDisplaySerializer(SessionSerializer):
|
||||
command_amount = serializers.IntegerField(read_only=True)
|
||||
command_amount = serializers.IntegerField(read_only=True, label=_('Command amount'))
|
||||
|
||||
class Meta(SessionSerializer.Meta):
|
||||
fields = SessionSerializer.Meta.fields + ['command_amount']
|
||||
extra_kwargs = {
|
||||
'command_amount': {'label': _('Command amount')},
|
||||
}
|
||||
|
||||
|
||||
class ReplaySerializer(serializers.Serializer):
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
<b>{% trans 'Command' %}:</b> {{ command }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{% trans 'Asset' %}:</b> {{ hostname }}({{ host_ip }})
|
||||
</p>
|
||||
<p>
|
||||
<b>{% trans 'User' %}:</b> {{ user }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{% trans 'Level' %}:</b> {{ risk_level }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{% trans 'Session' %}:</b> <a href="{{ session_detail_url}}?oid={{ oid }}">{% trans 'view' %}</a>
|
||||
</p>
|
||||
<div>
|
||||
{% for item, value in items.items %}
|
||||
<span class="cmd-item">
|
||||
<b>{{ item }}:</b> {{ value }}
|
||||
</span>
|
||||
<br />
|
||||
{% endfor %}
|
||||
<b>{% trans 'Session' %}:</b> <a href="{{ session_url }}" target="_blank">{% trans 'view' %}</a>
|
||||
<br />
|
||||
<b>{% trans 'Command' %}: </b><br />
|
||||
<pre>
|
||||
{{ command }}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
<b>{% trans 'User' %}:</b> {{ user }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{% trans 'Level' %}:</b> {{ risk_level }}
|
||||
</p>
|
||||
<div>
|
||||
<b>{% trans 'Command' %}: </b><br>
|
||||
{% for item, value in items.items %}
|
||||
<span class="cmd-item">
|
||||
<b>{{ item }}:</b> {{ value }}
|
||||
</span>
|
||||
<br />
|
||||
{% endfor %}
|
||||
<b>{% trans 'Command' %}: </b><br />
|
||||
<pre>
|
||||
{{ command }}
|
||||
{{ command }}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -17,7 +17,7 @@
|
|||
<ul>
|
||||
{% for asset, url in assets_with_url %}
|
||||
<li>
|
||||
<a href="{{ url }}">{{ asset }}</a>
|
||||
<a href="{{ url }}" target="_blank">{{ asset }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
@ -29,6 +29,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
|||
search_fields = [
|
||||
'title', 'action', 'type', 'status', 'applicant_display'
|
||||
]
|
||||
ordering_fields = ('title',)
|
||||
ordering = ('title', )
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(self.action)
|
||||
|
|
|
@ -43,18 +43,18 @@ class Handler(BaseHandler):
|
|||
apply_date_start = self.ticket.meta.get('apply_date_start')
|
||||
apply_date_expired = self.ticket.meta.get('apply_date_expired')
|
||||
applied_body = '''{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
'''.format(
|
||||
_('Applied category'), apply_category_display,
|
||||
_('Applied type'), apply_type_display,
|
||||
_('Applied application group'), ','.join(apply_applications),
|
||||
_('Applied system user group'), ','.join(apply_system_users),
|
||||
_('Applied date start'), apply_date_start.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
_('Applied date expired'), apply_date_expired.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
_('Applied date start'), apply_date_start,
|
||||
_('Applied date expired'), apply_date_expired,
|
||||
)
|
||||
return applied_body
|
||||
|
||||
|
@ -84,7 +84,7 @@ class Handler(BaseHandler):
|
|||
).format(
|
||||
self.ticket.title,
|
||||
self.ticket.applicant_display,
|
||||
str(self.ticket.processor),
|
||||
','.join([i['processor_display'] for i in self.ticket.process_map]),
|
||||
str(self.ticket.id)
|
||||
)
|
||||
permissions_data = {
|
||||
|
|
|
@ -47,8 +47,8 @@ class Handler(BaseHandler):
|
|||
_("Applied hostname group"), ','.join(apply_assets),
|
||||
_("Applied system user group"), ','.join(apply_system_users),
|
||||
_("Applied actions"), ','.join(apply_actions_display),
|
||||
_('Applied date start'), apply_date_start.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
_('Applied date expired'), apply_date_expired.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
_('Applied date start'), apply_date_start,
|
||||
_('Applied date expired'), apply_date_expired,
|
||||
)
|
||||
return applied_body
|
||||
|
||||
|
@ -69,15 +69,15 @@ class Handler(BaseHandler):
|
|||
str(self.ticket.__class__.__name__), str(self.ticket.id)
|
||||
)
|
||||
permission_comment = _(
|
||||
'Created by the ticket, '
|
||||
'ticket title: {}, '
|
||||
'ticket applicant: {}, '
|
||||
'ticket processor: {}, '
|
||||
'Created by the ticket '
|
||||
'ticket title: {} '
|
||||
'ticket applicant: {} '
|
||||
'ticket processor: {} '
|
||||
'ticket ID: {}'
|
||||
).format(
|
||||
self.ticket.title,
|
||||
self.ticket.applicant_display,
|
||||
str(self.ticket.processor),
|
||||
','.join([i['processor_display'] for i in self.ticket.process_map]),
|
||||
str(self.ticket.id)
|
||||
)
|
||||
|
||||
|
|
|
@ -23,16 +23,18 @@ class BaseHandler(object):
|
|||
|
||||
def _on_approve(self):
|
||||
if self.ticket.approval_step != len(self.ticket.process_map):
|
||||
self._send_processed_mail_to_applicant(self.ticket.processor)
|
||||
self.ticket.approval_step += 1
|
||||
self.ticket.create_related_node()
|
||||
self._send_applied_mail_to_assignees()
|
||||
is_finished = False
|
||||
else:
|
||||
self._send_processed_mail_to_applicant(self.ticket.processor)
|
||||
self.ticket.set_state_approve()
|
||||
self.ticket.set_status_closed()
|
||||
is_finished = True
|
||||
self._send_applied_mail_to_assignees()
|
||||
|
||||
self.__on_process(self.ticket.processor)
|
||||
self.ticket.save()
|
||||
return is_finished
|
||||
|
||||
def _on_reject(self):
|
||||
|
@ -105,28 +107,29 @@ class BaseHandler(object):
|
|||
return basic_body + meta_body
|
||||
|
||||
def _construct_basic_body(self):
|
||||
basic_body = '''{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
basic_body = '''
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
'''.format(
|
||||
_('Ticket title'), self.ticket.title,
|
||||
_('Ticket type'), self.ticket.get_type_display(),
|
||||
_('Ticket status'), self.ticket.get_status_display(),
|
||||
_('Ticket applicant'), self.ticket.applicant_display,
|
||||
)
|
||||
).strip()
|
||||
body = self.body_html_format.format(_("Ticket basic info"), basic_body)
|
||||
return body
|
||||
|
||||
def _construct_meta_body(self):
|
||||
body = ''
|
||||
open_body = self._base_construct_meta_body_of_open()
|
||||
open_body = self._base_construct_meta_body_of_open().strip()
|
||||
body += open_body
|
||||
return body
|
||||
|
||||
def _base_construct_meta_body_of_open(self):
|
||||
meta_body_of_open = getattr(
|
||||
self, '_construct_meta_body_of_open', lambda: _('No content')
|
||||
)()
|
||||
)().strip()
|
||||
body = self.body_html_format.format(_('Ticket applied info'), meta_body_of_open)
|
||||
return body
|
||||
|
|
|
@ -14,12 +14,13 @@ class Handler(BaseHandler):
|
|||
apply_from_cmd_filter_rule_id = self.ticket.meta.get('apply_from_cmd_filter_rule_id')
|
||||
apply_from_cmd_filter_id = self.ticket.meta.get('apply_from_cmd_filter_id')
|
||||
|
||||
applied_body = '''{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
{}: {},
|
||||
applied_body = '''
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
'''.format(
|
||||
_("Applied run user"), apply_run_user,
|
||||
_("Applied run asset"), apply_run_asset,
|
||||
|
|
|
@ -9,8 +9,9 @@ class Handler(BaseHandler):
|
|||
apply_login_user = self.ticket.meta.get('apply_login_user')
|
||||
apply_login_asset = self.ticket.meta.get('apply_login_asset')
|
||||
apply_login_system_user = self.ticket.meta.get('apply_login_system_user')
|
||||
applied_body = '''{}: {},
|
||||
{}: {},
|
||||
applied_body = '''
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
'''.format(
|
||||
_("Applied login user"), apply_login_user,
|
||||
|
|
|
@ -9,8 +9,9 @@ class Handler(BaseHandler):
|
|||
apply_login_ip = self.ticket.meta.get('apply_login_ip')
|
||||
apply_login_city = self.ticket.meta.get('apply_login_city')
|
||||
apply_login_datetime = self.ticket.meta.get('apply_login_datetime')
|
||||
applied_body = '''{}: {},
|
||||
{}: {},
|
||||
applied_body = '''
|
||||
{}: {}
|
||||
{}: {}
|
||||
{}: {}
|
||||
'''.format(
|
||||
_("Applied login IP"), apply_login_ip,
|
||||
|
|
|
@ -2,7 +2,7 @@ from urllib.parse import urljoin
|
|||
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import const
|
||||
from notifications.notifications import UserMessage
|
||||
|
@ -18,12 +18,17 @@ class BaseTicketMessage(UserMessage):
|
|||
content_title: str
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return self.title.format(self.ticket.title, self.ticket.get_type_display())
|
||||
def ticket_detail_url(self):
|
||||
tp = self.ticket.type
|
||||
return urljoin(settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(self.ticket.id)))
|
||||
|
||||
@property
|
||||
def ticket_detail_url(self):
|
||||
return urljoin(settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(self.ticket.id)))
|
||||
def content_title(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
context = dict(
|
||||
|
@ -39,12 +44,10 @@ class BaseTicketMessage(UserMessage):
|
|||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
return cls(None)
|
||||
return None
|
||||
|
||||
|
||||
class TicketAppliedToAssignee(BaseTicketMessage):
|
||||
title = _('New Ticket - {} ({})')
|
||||
|
||||
def __init__(self, user, ticket):
|
||||
self.ticket = ticket
|
||||
super().__init__(user)
|
||||
|
@ -55,6 +58,13 @@ class TicketAppliedToAssignee(BaseTicketMessage):
|
|||
str(self.ticket.applicant_display)
|
||||
)
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
title = _('New Ticket - {} ({})').format(
|
||||
self.ticket.title, self.ticket.get_type_display()
|
||||
)
|
||||
return title
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from .models import Ticket
|
||||
|
@ -65,8 +75,6 @@ class TicketAppliedToAssignee(BaseTicketMessage):
|
|||
|
||||
|
||||
class TicketProcessedToApplicant(BaseTicketMessage):
|
||||
title = _('Ticket has processed - {} ({})')
|
||||
|
||||
def __init__(self, user, ticket, processor):
|
||||
self.ticket = ticket
|
||||
self.processor = processor
|
||||
|
@ -76,6 +84,13 @@ class TicketProcessedToApplicant(BaseTicketMessage):
|
|||
def content_title(self):
|
||||
return _('Your ticket has been processed, processor - {}').format(str(self.processor))
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
title = _('Ticket has processed - {} ({})').format(
|
||||
self.ticket.title, self.ticket.get_type_display()
|
||||
)
|
||||
return title
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from .models import Ticket
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{% load i18n %}
|
||||
<div>
|
||||
<p class="ticket-info">
|
||||
{{ ticket_info }}
|
||||
</p>
|
||||
<div>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
</p>
|
||||
|
||||
{{ body | safe }}
|
||||
</div>
|
||||
<div>
|
||||
<a href={{ ticket_detail_url }}>
|
||||
{% trans 'Click here to review' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -1,13 +1,14 @@
|
|||
{% load i18n %}
|
||||
<div>
|
||||
<p>
|
||||
{{ title}}
|
||||
{{ title | safe }}
|
||||
</p>
|
||||
<div>
|
||||
{{ body}}
|
||||
{{ body | safe }}
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<a href={{ ticket_detail_url }}>
|
||||
<a href="{{ ticket_detail_url }}" target="_blank">
|
||||
{% trans 'Click here to review' %}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -16,3 +16,5 @@ class UserGroupViewSet(OrgBulkModelViewSet):
|
|||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = UserGroupSerializer
|
||||
ordering_fields = ('name', )
|
||||
ordering = ('name', )
|
||||
|
|
|
@ -8,7 +8,6 @@ from rest_framework.response import Response
|
|||
from rest_framework_bulk import BulkModelViewSet
|
||||
from django.db.models import Prefetch
|
||||
|
||||
from users.notifications import ResetMFAMsg
|
||||
from common.permissions import (
|
||||
IsOrgAdmin, IsOrgAdminOrAppUser,
|
||||
CanUpdateDeleteUser, IsSuperUser
|
||||
|
@ -18,9 +17,10 @@ from common.utils import get_logger
|
|||
from orgs.utils import current_org
|
||||
from orgs.models import ROLE as ORG_ROLE, OrganizationMember
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from .mixins import UserQuerysetMixin
|
||||
from ..notifications import ResetMFAMsg
|
||||
from .. import serializers
|
||||
from ..serializers import UserSerializer, MiniUserSerializer, InviteSerializer
|
||||
from .mixins import UserQuerysetMixin
|
||||
from ..models import User
|
||||
from ..signals import post_user_create
|
||||
from ..filters import OrgRoleUserFilterBackend, UserFilter
|
||||
|
@ -42,6 +42,8 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
|
|||
'invite': InviteSerializer,
|
||||
}
|
||||
extra_filter_backends = [OrgRoleUserFilterBackend]
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().prefetch_related(
|
||||
|
@ -128,9 +130,9 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
|
|||
return super().perform_bulk_update(serializer)
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=(IsOrgAdmin,))
|
||||
def suggestion(self, request):
|
||||
def suggestion(self, *args, **kwargs):
|
||||
queryset = User.objects.exclude(role=User.ROLE.APP)
|
||||
queryset = self.filter_queryset(queryset)[:3]
|
||||
queryset = self.filter_queryset(queryset)[:6]
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
@ -206,6 +208,7 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
|
|||
if user == request.user:
|
||||
msg = _("Could not reset self otp, use profile reset instead")
|
||||
return Response({"error": msg}, status=401)
|
||||
|
||||
if user.mfa_enabled:
|
||||
user.reset_mfa()
|
||||
user.save()
|
||||
|
|
|
@ -17,7 +17,6 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.utils import timezone
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from acls.models import LoginACL
|
||||
from orgs.utils import current_org
|
||||
from orgs.models import OrganizationMember, Organization
|
||||
from common.exceptions import JMSException
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from datetime import datetime
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.utils import timezone
|
||||
|
@ -10,6 +9,38 @@ from common.utils import reverse, get_request_ip_or_data, get_request_user_agent
|
|||
from notifications.notifications import UserMessage
|
||||
|
||||
|
||||
class UserCreatedMsg(UserMessage):
|
||||
def get_html_msg(self) -> dict:
|
||||
user = self.user
|
||||
subject = _('Create account successfully')
|
||||
if settings.EMAIL_CUSTOM_USER_CREATED_SUBJECT:
|
||||
subject = settings.EMAIL_CUSTOM_USER_CREATED_SUBJECT
|
||||
|
||||
honorific = settings.EMAIL_CUSTOM_USER_CREATED_HONORIFIC or _('Hello {}').format(user.name)
|
||||
signature = settings.EMAIL_CUSTOM_USER_CREATED_SIGNATURE or 'JumpServer'
|
||||
|
||||
context = {
|
||||
'honorific': honorific,
|
||||
'signature': signature,
|
||||
'username': user.username,
|
||||
'rest_password_url': reverse('authentication:reset-password', external=True),
|
||||
'rest_password_token': user.generate_reset_token(),
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
message = render_to_string('users/_msg_user_created.html', context)
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
user = cls.get_test_user()
|
||||
return cls(user)
|
||||
|
||||
|
||||
class ResetPasswordMsg(UserMessage):
|
||||
def __init__(self, user):
|
||||
super().__init__(user)
|
||||
|
@ -71,18 +102,17 @@ class ResetPasswordSuccessMsg(UserMessage):
|
|||
|
||||
|
||||
class PasswordExpirationReminderMsg(UserMessage):
|
||||
update_password_url = urljoin(settings.SITE_URL, '/ui/#/users/profile/?activeTab=PasswordUpdate')
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
user = self.user
|
||||
subject = _('Password is about expire')
|
||||
|
||||
date_password_expired_local = timezone.localtime(user.date_password_expired)
|
||||
update_password_url = urljoin(settings.SITE_URL, '/ui/#/users/profile/?activeTab=PasswordUpdate')
|
||||
date_password_expired = date_password_expired_local.strftime('%Y-%m-%d %H:%M:%S')
|
||||
context = {
|
||||
'name': user.name,
|
||||
'date_password_expired': date_password_expired,
|
||||
'update_password_url': self.update_password_url,
|
||||
'update_password_url': update_password_url,
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
|
@ -125,9 +155,10 @@ class UserExpirationReminderMsg(UserMessage):
|
|||
class ResetSSHKeyMsg(UserMessage):
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _('Reset SSH Key')
|
||||
update_url = urljoin(settings.SITE_URL, '/ui/#/users/profile/?activeTab=SSHUpdate')
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
'url': update_url,
|
||||
}
|
||||
message = render_to_string('users/_msg_reset_ssh_key.html', context)
|
||||
return {
|
||||
|
@ -147,7 +178,7 @@ class ResetMFAMsg(UserMessage):
|
|||
subject = _('Reset MFA')
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
'url': reverse('authentication:user-otp-enable-start', external=True),
|
||||
}
|
||||
message = render_to_string('users/_msg_reset_mfa.html', context)
|
||||
return {
|
||||
|
|
|
@ -13,10 +13,6 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
{% trans 'If your password has expired, please click' %}
|
||||
{% trans 'If your password has expired, please click the link below to' %}
|
||||
<a href="{{ forget_password_url }}?email={{ email }}">{% trans 'Reset password' %}</a>
|
||||
{% trans 'to apply for a password reset email.' %}
|
||||
</p>
|
||||
---
|
||||
<br>
|
||||
<a href="{{ login_url }}">{% trans 'Login direct' %}</a>
|
|
@ -4,9 +4,9 @@
|
|||
{% trans 'Hello' %} {{ name }},
|
||||
</p>
|
||||
<p>
|
||||
{% trans 'Your MFA has been reset by site administrator.' %} <br />
|
||||
{% trans 'Please login and reset your MFA.' %}
|
||||
{% trans 'Your MFA has been reset by site administrator' %} <br />
|
||||
{% trans 'Please click the link below to set' %}
|
||||
<br>
|
||||
<br>
|
||||
<a href="{{ url }}" class='showLink'>{% trans 'Click here set' %}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ login_url }}">{% trans 'Login direct' %}</a>
|
||||
</p>
|
|
@ -4,9 +4,9 @@
|
|||
{% trans 'Hello' %} {{ name }},
|
||||
</p>
|
||||
<p>
|
||||
{% trans 'Your ssh public key has been reset by site administrator.' %} <br />
|
||||
{% trans 'Please login and reset your ssh public key.' %}
|
||||
{% trans 'Your ssh public key has been reset by site administrator' %} <br />
|
||||
{% trans 'Please click the link below to set' %}
|
||||
<br>
|
||||
<br>
|
||||
<a href="{{ url }}" class='showLink'>{% trans 'Click here set' %}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ login_url }}">{% trans 'Login direct' %}</a>
|
||||
</p>
|
|
@ -0,0 +1,20 @@
|
|||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
{{ honorific }}:
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p>{% trans 'Your account has been created successfully' %}</p>
|
||||
<p>
|
||||
{% trans 'Username' %}: {{ username }} <br />
|
||||
{% trans 'Password' %}:
|
||||
<a href="{{ rest_password_url}}?token={{ rest_password_token }}">
|
||||
{% trans 'click here to set your password' %}
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
{% trans 'This link is valid for 1 hour. After it expires' %}
|
||||
<a href="{{ forget_password_url }}?email={{ email }}">{% trans 'request new one' %}</a>
|
||||
</p>
|
||||
</div>
|
|
@ -8,67 +8,24 @@ import logging
|
|||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.tasks import send_mail_async
|
||||
from common.utils import reverse, get_object_or_none, get_request_ip_or_data, get_request_user_agent
|
||||
from common.utils import reverse, get_object_or_none
|
||||
from .models import User
|
||||
|
||||
|
||||
logger = logging.getLogger('jumpserver')
|
||||
|
||||
|
||||
def construct_user_created_email_body(user):
|
||||
default_body = _("""
|
||||
<div>
|
||||
<p>Your account has been created successfully</p>
|
||||
<div>
|
||||
Username: %(username)s
|
||||
<br/>
|
||||
Password: <a href="%(rest_password_url)s?token=%(rest_password_token)s">
|
||||
click here to set your password</a>
|
||||
(This link is valid for 1 hour. After it expires, <a href="%(forget_password_url)s?email=%(email)s">request new one</a>)
|
||||
</div>
|
||||
<div>
|
||||
<p>---</p>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
</div>
|
||||
</div>
|
||||
""") % {
|
||||
'username': user.username,
|
||||
'rest_password_url': reverse('authentication:reset-password', external=True),
|
||||
'rest_password_token': user.generate_reset_token(),
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
|
||||
if settings.EMAIL_CUSTOM_USER_CREATED_BODY:
|
||||
custom_body = '<p style="text-indent:2em">' + settings.EMAIL_CUSTOM_USER_CREATED_BODY + '</p>'
|
||||
else:
|
||||
custom_body = ''
|
||||
body = custom_body + default_body
|
||||
return body
|
||||
|
||||
|
||||
def send_user_created_mail(user):
|
||||
from .notifications import UserCreatedMsg
|
||||
|
||||
recipient_list = [user.email]
|
||||
subject = _('Create account successfully')
|
||||
if settings.EMAIL_CUSTOM_USER_CREATED_SUBJECT:
|
||||
subject = settings.EMAIL_CUSTOM_USER_CREATED_SUBJECT
|
||||
msg = UserCreatedMsg.html_msg
|
||||
subject = msg['subject']
|
||||
message = msg['message']
|
||||
|
||||
honorific = '<p>' + _('Hello %(name)s') % {'name': user.name} + ':</p>'
|
||||
if settings.EMAIL_CUSTOM_USER_CREATED_HONORIFIC:
|
||||
honorific = '<p>' + settings.EMAIL_CUSTOM_USER_CREATED_HONORIFIC + ':</p>'
|
||||
|
||||
body = construct_user_created_email_body(user)
|
||||
|
||||
signature = '<p style="float:right">jumpserver</p>'
|
||||
if settings.EMAIL_CUSTOM_USER_CREATED_SIGNATURE:
|
||||
signature = '<p style="float:right">' + settings.EMAIL_CUSTOM_USER_CREATED_SIGNATURE + '</p>'
|
||||
|
||||
message = honorific + body + signature
|
||||
if settings.DEBUG:
|
||||
try:
|
||||
print(message)
|
||||
|
|
Loading…
Reference in New Issue