Merge pull request #7068 from jumpserver/dev

v2.15.0-rc2
pull/7119/head
Jiangjie.Bai 2021-10-25 15:07:25 +08:00 committed by GitHub
commit f673fed706
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 1488 additions and 1215 deletions

View File

@ -11,5 +11,5 @@ class LoginAclFilter(BaseFilterSet):
class Meta:
model = LoginACL
fields = (
'name', 'user', 'user_display'
'name', 'user', 'user_display', 'action'
)

View File

@ -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', ])

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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'):

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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__)

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 *

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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__)

View 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__)

View File

@ -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

View File

@ -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__)

View 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, 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__)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')
]

View File

@ -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 = []

View File

@ -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>

View File

@ -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:

View File

@ -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(

View File

@ -21,3 +21,5 @@ class AssetPermissionViewSet(OrgBulkModelViewSet):
serializer_class = serializers.AssetPermissionSerializer
filterset_class = AssetPermissionFilter
search_fields = ('name',)
ordering_fields = ('name',)
ordering = ('name', )

View File

@ -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

View File

@ -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()

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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']

View File

@ -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']

View File

@ -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,)

View File

@ -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):

View File

@ -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 {

View File

@ -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):

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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 = {

View File

@ -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)
)

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -16,3 +16,5 @@ class UserGroupViewSet(OrgBulkModelViewSet):
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = UserGroupSerializer
ordering_fields = ('name', )
ordering = ('name', )

View File

@ -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()

View File

@ -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

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)