From 121f56f44bd2f0dd5fad205053a70909ec77b460 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 11 Jan 2018 20:10:27 +0800 Subject: [PATCH] =?UTF-8?q?[Feature]=20=E6=B7=BB=E5=8A=A0setting=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/signals_handler.py | 3 +- apps/common/api.py | 42 +++++++++++ apps/common/apps.py | 6 ++ apps/common/forms.py | 74 +++++++++++++++++++ apps/common/mixins.py | 14 +++- apps/common/models.py | 47 +++++++++++- apps/common/permissions.py | 52 +++++++++++++ apps/common/serializers.py | 10 +++ apps/common/signals.py | 6 ++ apps/common/signals_handler.py | 27 +++++++ .../templates/common/email_setting.html | 73 ++++++++++++++++++ apps/common/templatetags/common_tags.py | 9 +++ apps/common/urls/api_urls.py | 11 +++ apps/common/urls/view_urls.py | 11 +++ apps/common/views.py | 33 ++++++++- apps/jumpserver/settings.py | 9 --- apps/jumpserver/urls.py | 5 +- apps/users/models/__init__.py | 2 +- apps/users/models/group.py | 6 -- apps/users/models/user.py | 3 +- 20 files changed, 417 insertions(+), 26 deletions(-) create mode 100644 apps/common/api.py create mode 100644 apps/common/forms.py create mode 100644 apps/common/permissions.py create mode 100644 apps/common/serializers.py create mode 100644 apps/common/signals.py create mode 100644 apps/common/signals_handler.py create mode 100644 apps/common/templates/common/email_setting.html create mode 100644 apps/common/urls/api_urls.py create mode 100644 apps/common/urls/view_urls.py diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 63dc3e882..171cfa6d9 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- # - -from django.db.models.signals import post_save, post_init, m2m_changed, pre_save +from django.db.models.signals import post_save, post_init from django.dispatch import receiver from django.utils.translation import gettext as _ diff --git a/apps/common/api.py b/apps/common/api.py new file mode 100644 index 000000000..270c81095 --- /dev/null +++ b/apps/common/api.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +from rest_framework.views import APIView +from rest_framework.views import Response +from django.core.mail import get_connection, send_mail +from django.utils.translation import ugettext_lazy as _ + +from .permissions import IsSuperUser +from .serializers import MailTestSerializer + + +class MailTestingAPI(APIView): + permission_classes = (IsSuperUser,) + serializer_class = MailTestSerializer + success_message = _("Test mail sent to {}, please check") + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + email_host_user = serializer.validated_data["EMAIL_HOST_USER"] + kwargs = { + "host": serializer.validated_data["EMAIL_HOST"], + "port": serializer.validated_data["EMAIL_PORT"], + "username": serializer.validated_data["EMAIL_HOST_USER"], + "password": serializer.validated_data["EMAIL_HOST_PASSWORD"], + "use_ssl": serializer.validated_data["EMAIL_USE_SSL"], + "use_tls": serializer.validated_data["EMAIL_USE_TLS"] + } + connection = get_connection(timeout=5, **kwargs) + + try: + connection.open() + except Exception as e: + return Response({"error": str(e)}, status=401) + + try: + send_mail("Test", "Test smtp setting", email_host_user, + [email_host_user], connection=connection) + except Exception as e: + return Response({"error": str(e)}, status=401) + + return Response({"msg": self.success_message.format(email_host_user)}) diff --git a/apps/common/apps.py b/apps/common/apps.py index 6664a6438..bc6db2151 100644 --- a/apps/common/apps.py +++ b/apps/common/apps.py @@ -5,3 +5,9 @@ from django.apps import AppConfig class CommonConfig(AppConfig): name = 'common' + + def ready(self): + from . import signals_handler + from .signals import django_ready + django_ready.send(self.__class__) + return super().ready() diff --git a/apps/common/forms.py b/apps/common/forms.py new file mode 100644 index 000000000..60cb1ae37 --- /dev/null +++ b/apps/common/forms.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# +import json + +from django import forms +from django.utils.translation import ugettext_lazy as _ +from django.db import transaction + +from .models import Setting + + +def to_model_value(value): + try: + return json.dumps(value) + except json.JSONDecodeError: + return None + + +def to_form_value(value): + try: + return json.loads(value) + except json.JSONDecodeError: + return '' + + +class BaseForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.is_bound: + settings = Setting.objects.all() + for name, field in self.fields.items(): + db_value = getattr(settings, name).value + if db_value: + field.initial = to_form_value(db_value) + + def save(self): + if not self.is_bound: + raise ValueError("Form is not bound") + + if self.is_valid(): + with transaction.atomic(): + for name, value in self.cleaned_data.items(): + field = self.fields[name] + if isinstance(field.widget, forms.PasswordInput) and not value: + continue + defaults = { + 'name': name, + 'value': to_model_value(value) + } + Setting.objects.update_or_create(defaults=defaults, name=name) + else: + raise ValueError(self.errors) + + +class EmailSettingForm(BaseForm): + EMAIL_HOST = forms.CharField( + max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org' + ) + EMAIL_PORT = forms.CharField(max_length=5, label=_("SMTP port"), initial=25) + EMAIL_HOST_USER = forms.CharField( + max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org' + ) + EMAIL_HOST_PASSWORD = forms.CharField( + max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput, + required=False, help_text=_("Some provider use token except password") + ) + EMAIL_USE_SSL = forms.BooleanField( + label=_("Use SSL"), initial=False, required=False, + help_text=_("If SMTP port is 465, may be select") + ) + EMAIL_USE_TLS = forms.BooleanField( + label=_("Use TLS"), initial=False, required=False, + help_text=_("If SMTP port is 587, may be select") + ) diff --git a/apps/common/mixins.py b/apps/common/mixins.py index 1371cf65b..2832424c6 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -1,10 +1,10 @@ # coding: utf-8 -import inspect from django.db import models from django.http import JsonResponse from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.mixins import UserPassesTestMixin class NoDeleteQuerySet(models.query.QuerySet): @@ -113,4 +113,14 @@ class DatetimeSearchMixin: ) else: self.date_to = timezone.now() - return super().get(request, *args, **kwargs) \ No newline at end of file + return super().get(request, *args, **kwargs) + + +class AdminUserRequiredMixin(UserPassesTestMixin): + def test_func(self): + if not self.request.user.is_authenticated: + return False + elif not self.request.user.is_superuser: + self.raise_exception = True + return False + return True diff --git a/apps/common/models.py b/apps/common/models.py index beeb30826..3d6ee6008 100644 --- a/apps/common/models.py +++ b/apps/common/models.py @@ -1,2 +1,47 @@ -from django.db import models +import json +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + + +class SettingQuerySet(models.QuerySet): + def __getattr__(self, item): + instances = self.filter(name=item) + if len(instances) == 1: + return instances[0] + else: + return Setting() + + +class SettingManager(models.Manager): + def get_queryset(self): + return SettingQuerySet(self.model, using=self._db) + + +class Setting(models.Model): + name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) + value = models.TextField(verbose_name=_("Value")) + enabled = models.BooleanField(verbose_name=_("Enabled"), default=True) + comment = models.TextField(verbose_name=_("Comment")) + + objects = SettingManager() + + def __str__(self): + return self.name + + @classmethod + def refresh_all_settings(cls): + settings_list = cls.objects.all() + for setting in settings_list: + setting.refresh_setting() + + def refresh_setting(self): + try: + value = json.loads(self.value) + except json.JSONDecodeError: + return + setattr(settings, self.name, value) + + class Meta: + db_table = "settings" diff --git a/apps/common/permissions.py b/apps/common/permissions.py new file mode 100644 index 000000000..6a1cb8230 --- /dev/null +++ b/apps/common/permissions.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import permissions + + +class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission): + """Allows access to valid user, is active and not expired""" + + def has_permission(self, request, view): + return super(IsValidUser, self).has_permission(request, view) \ + and request.user.is_valid + + +class IsAppUser(IsValidUser): + """Allows access only to app user """ + + def has_permission(self, request, view): + return super(IsAppUser, self).has_permission(request, view) \ + and request.user.is_app + + +class IsSuperUser(IsValidUser): + """Allows access only to superuser""" + + def has_permission(self, request, view): + return super(IsSuperUser, self).has_permission(request, view) \ + and request.user.is_superuser + + +class IsSuperUserOrAppUser(IsValidUser): + """Allows access between superuser and app user""" + + def has_permission(self, request, view): + return super(IsSuperUserOrAppUser, self).has_permission(request, view) \ + and (request.user.is_superuser or request.user.is_app) + + +class IsSuperUserOrAppUserOrUserReadonly(IsSuperUserOrAppUser): + def has_permission(self, request, view): + if IsValidUser.has_permission(self, request, view) \ + and request.method in permissions.SAFE_METHODS: + return True + else: + return IsSuperUserOrAppUser.has_permission(self, request, view) + + +class IsCurrentUserOrReadOnly(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + return obj == request.user diff --git a/apps/common/serializers.py b/apps/common/serializers.py new file mode 100644 index 000000000..37e6555a3 --- /dev/null +++ b/apps/common/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + + +class MailTestSerializer(serializers.Serializer): + EMAIL_HOST = serializers.CharField(max_length=1024, required=True) + EMAIL_PORT = serializers.IntegerField(default=25) + EMAIL_HOST_USER = serializers.CharField(max_length=1024) + EMAIL_HOST_PASSWORD = serializers.CharField() + EMAIL_USE_SSL = serializers.BooleanField(default=False) + EMAIL_USE_TLS = serializers.BooleanField(default=False) diff --git a/apps/common/signals.py b/apps/common/signals.py new file mode 100644 index 000000000..de8a84139 --- /dev/null +++ b/apps/common/signals.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# + +from django.dispatch import Signal + +django_ready = Signal() diff --git a/apps/common/signals_handler.py b/apps/common/signals_handler.py new file mode 100644 index 000000000..6b54706db --- /dev/null +++ b/apps/common/signals_handler.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# + +from django.dispatch import receiver +from django.db.models.signals import post_save + +from .models import Setting +from .utils import get_logger +from .signals import django_ready + + +logger = get_logger(__file__) + + +@receiver(post_save, sender=Setting, dispatch_uid="my_unique_identifier") +def refresh_settings_on_changed(sender, instance=None, **kwargs): + logger.debug("Receive setting item change") + logger.debug(" - refresh setting: {}".format(instance.name)) + if instance: + instance.refresh_setting() + + +@receiver(django_ready, dispatch_uid="my_unique_identifier") +def refresh_all_settings_on_django_ready(sender, **kwargs): + logger.debug("Receive django ready signal") + logger.debug(" - fresh all settings") + Setting.refresh_all_settings() diff --git a/apps/common/templates/common/email_setting.html b/apps/common/templates/common/email_setting.html new file mode 100644 index 000000000..d6d7fdf16 --- /dev/null +++ b/apps/common/templates/common/email_setting.html @@ -0,0 +1,73 @@ +{% extends 'base.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} +{% load common_tags %} + +{% block custom_head_css_js %} + + +{% endblock %} +{% block content %} +
+
+
+
+ +
+
+
+
+ {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} + {% csrf_token %} + {% for field in form %} + {% if not field.field|is_bool_field %} + {% bootstrap_field field layout="horizontal" %} + {% else %} +
+ +
+
+ {{ field }} +
+
+ {{ field.help_text }} +
+
+
+ {% endif %} + {% endfor %} +
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/common/templatetags/common_tags.py b/apps/common/templatetags/common_tags.py index 9c3e58871..c10c228c8 100644 --- a/apps/common/templatetags/common_tags.py +++ b/apps/common/templatetags/common_tags.py @@ -4,6 +4,7 @@ from django import template from django.utils import timezone from django.utils.translation import gettext as _ from django.utils.html import escape +from django import forms register = template.Library() @@ -83,3 +84,11 @@ def time_util_with_seconds(date_from, date_to): return '{} h'.format(seconds//3600) else: return '' + + +@register.filter +def is_bool_field(field): + if isinstance(field, forms.BooleanField): + return True + else: + return False diff --git a/apps/common/urls/api_urls.py b/apps/common/urls/api_urls.py new file mode 100644 index 000000000..81acabae1 --- /dev/null +++ b/apps/common/urls/api_urls.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import + +from django.conf.urls import url + +from .. import api + +app_name = 'common' + +urlpatterns = [ + url(r'^v1/mail/testing/$', api.MailTestingAPI.as_view(), name='mail-testing'), +] diff --git a/apps/common/urls/view_urls.py b/apps/common/urls/view_urls.py new file mode 100644 index 000000000..c2ea04207 --- /dev/null +++ b/apps/common/urls/view_urls.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import + +from django.conf.urls import url + +from .. import views + +app_name = 'common' + +urlpatterns = [ + url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'), +] diff --git a/apps/common/views.py b/apps/common/views.py index 4cee32ba7..f6e32af5e 100644 --- a/apps/common/views.py +++ b/apps/common/views.py @@ -1,2 +1,33 @@ -from __future__ import absolute_import, unicode_literals +from django.views.generic import View +from django.shortcuts import render +from django.contrib import messages +from django.utils.translation import ugettext as _ +from .forms import EmailSettingForm +from .mixins import AdminUserRequiredMixin + + +class EmailSettingView(AdminUserRequiredMixin, View): + form_class = EmailSettingForm + template_name = "common/email_setting.html" + + def get(self, request): + context = { + 'app': 'settings', + 'action': 'Email setting', + "form": EmailSettingForm(), + } + return render(request, self.template_name, context) + + def post(self, request): + form = self.form_class(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("Update email setting successfully")) + + context = { + 'app': 'settings', + 'action': 'Email setting', + "form": EmailSettingForm(), + } + return render(request, self.template_name, context) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index ce2d06ed0..5d265aed3 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -121,15 +121,6 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' # Database # https://docs.djangoproject.com/en/1.10/ref/settings/#databases -# if CONFIG.DB_ENGINE == 'sqlite': -# DATABASES = { -# 'default': { -# 'ENGINE': 'django.db.backends.sqlite3', -# 'NAME': CONFIG.DB_NAME or os.path.join(BASE_DIR, 'data', 'db.sqlite3'), -# 'ATOMIC_REQUESTS': True, -# } -# } - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.{}'.format(CONFIG.DB_ENGINE), diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 839492e7a..2eb54d87d 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -19,6 +19,8 @@ urlpatterns = [ url(r'^perms/', include('perms.urls.views_urls', namespace='perms')), url(r'^terminal/', include('terminal.urls.views_urls', namespace='terminal')), url(r'^ops/', include('ops.urls.view_urls', namespace='ops')), + url(r'^settings/', include('common.urls.view_urls', namespace='settings')), + url(r'^common/', include('common.urls.view_urls', namespace='common')), # Api url view map url(r'^api/users/', include('users.urls.api_urls', namespace='api-users')), @@ -26,13 +28,12 @@ urlpatterns = [ url(r'^api/perms/', include('perms.urls.api_urls', namespace='api-perms')), url(r'^api/terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), url(r'^api/ops/', include('ops.urls.api_urls', namespace='api-ops')), + url(r'^api/common/', include('common.urls.api_urls', namespace='api-common')), # External apps url url(r'^captcha/', include('captcha.urls')), - ] - if settings.DEBUG: urlpatterns += [ url(r'^docs/', schema_view, name="docs"), diff --git a/apps/users/models/__init__.py b/apps/users/models/__init__.py index 269f68bd4..f3c9d1941 100644 --- a/apps/users/models/__init__.py +++ b/apps/users/models/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -from .group import * from .user import * +from .group import * from .authentication import * from .utils import * diff --git a/apps/users/models/group.py b/apps/users/models/group.py index a0c7a8b0f..b4b7aacc1 100644 --- a/apps/users/models/group.py +++ b/apps/users/models/group.py @@ -20,12 +20,6 @@ class UserGroup(NoDeleteModelMixin): def __str__(self): return self.name - def delete(self, using=None, keep_parents=False): - if self.name != 'Default': - self.users.clear() - return super(UserGroup, self).delete() - return True - class Meta: ordering = ['name'] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 899cec4cb..031e4fa77 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -13,7 +13,6 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.shortcuts import reverse -from .group import UserGroup from common.utils import get_signer, date_expired_default @@ -35,7 +34,7 @@ class User(AbstractUser): username = models.CharField(max_length=128, unique=True, verbose_name=_('Username')) name = models.CharField(max_length=128, verbose_name=_('Name')) email = models.EmailField(max_length=128, unique=True, verbose_name=_('Email')) - groups = models.ManyToManyField(UserGroup, related_name='users', blank=True, verbose_name=_('User group')) + groups = models.ManyToManyField('users.UserGroup', related_name='users', blank=True, verbose_name=_('User group')) role = models.CharField(choices=ROLE_CHOICES, default='User', max_length=10, blank=True, verbose_name=_('Role')) avatar = models.ImageField(upload_to="avatar", null=True, verbose_name=_('Avatar')) wechat = models.CharField(max_length=128, blank=True, verbose_name=_('Wechat'))