diff --git a/apps/assets/views/admin_user.py b/apps/assets/views/admin_user.py index e3f6bc6b3..ce7b8bfb4 100644 --- a/apps/assets/views/admin_user.py +++ b/apps/assets/views/admin_user.py @@ -85,7 +85,7 @@ class AdminUserDetailView(AdminUserRequiredMixin, DetailView): class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView): - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE template_name = 'assets/admin_user_assets.html' context_object_name = 'admin_user' object = None diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index d251b7a15..b2f57e323 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -92,7 +92,7 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): class AssetModalListView(AdminUserRequiredMixin, ListView): - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE model = Asset context_object_name = 'asset_modal_list' template_name = 'assets/asset_modal_list.html' diff --git a/apps/common/api.py b/apps/common/api.py index 270c81095..e8680e85e 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- # +import json + from rest_framework.views import APIView from rest_framework.views import Response +from ldap3 import Server, Connection from django.core.mail import get_connection, send_mail from django.utils.translation import ugettext_lazy as _ +from django.conf import settings from .permissions import IsSuperUser -from .serializers import MailTestSerializer +from .serializers import MailTestSerializer, LDAPTestSerializer class MailTestingAPI(APIView): @@ -27,7 +31,6 @@ class MailTestingAPI(APIView): "use_tls": serializer.validated_data["EMAIL_USE_TLS"] } connection = get_connection(timeout=5, **kwargs) - try: connection.open() except Exception as e: @@ -40,3 +43,68 @@ class MailTestingAPI(APIView): return Response({"error": str(e)}, status=401) return Response({"msg": self.success_message.format(email_host_user)}) + else: + return Response({"error": str(serializer.errors)}, status=401) + + +class LDAPTestingAPI(APIView): + permission_classes = (IsSuperUser,) + serializer_class = LDAPTestSerializer + success_message = _("Test ldap success") + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + host = serializer.validated_data["AUTH_LDAP_SERVER_URI"] + bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"] + password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"] + use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False) + search_ou = serializer.validated_data["AUTH_LDAP_SEARCH_OU"] + search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"] + attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] + + print(serializer.validated_data) + + try: + attr_map = json.loads(attr_map) + except json.JSONDecodeError: + return Response({"error": "AUTH_LDAP_USER_ATTR_MAP not valid"}, status=401) + + server = Server(host, use_ssl=use_ssl) + conn = Connection(server, bind_dn, password) + try: + conn.bind() + except Exception as e: + return Response({"error": str(e)}, status=401) + + print(search_ou) + print(search_filter % ({"user": "*"})) + print(attr_map.values()) + ok = conn.search(search_ou, search_filter % ({"user": "*"}), + attributes=list(attr_map.values())) + if not ok: + return Response({"error": "Search no entry matched"}, status=401) + + users = [] + for entry in conn.entries: + user = {} + for attr, mapping in attr_map.items(): + if hasattr(entry, mapping): + user[attr] = getattr(entry, mapping) + users.append(user) + if len(users) > 0: + return Response({"msg": "Match {} s users".format(len(users))}) + else: + return Response({"error": "Have user but attr mapping error"}, status=401) + else: + return Response({"error": str(serializer.errors)}, status=401) + + +class DjangoSettingsAPI(APIView): + def get(self, request): + configs = {} + for i in dir(settings): + if i.isupper(): + configs[i] = str(getattr(settings, i)) + return Response(configs) + diff --git a/apps/common/fields.py b/apps/common/fields.py new file mode 100644 index 000000000..36a8bdf9a --- /dev/null +++ b/apps/common/fields.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +import json + +from django import forms +from django.utils import six +from django.core.exceptions import ValidationError + + +class DictField(forms.Field): + widget = forms.Textarea + + def to_python(self, value): + """Returns a Python boolean object.""" + # Explicitly check for the string 'False', which is what a hidden field + # will submit for False. Also check for '0', since this is what + # RadioSelect will provide. Because bool("True") == bool('1') == True, + # we don't need to handle that explicitly. + if isinstance(value, six.string_types): + try: + print(value) + value = json.loads(value) + return value + except json.JSONDecodeError: + pass + value = {} + return value + + def validate(self, value): + print(value) + if not value and self.required: + raise ValidationError(self.error_messages['required'], code='required') + + def has_changed(self, initial, data): + # Sometimes data or initial may be a string equivalent of a boolean + # so we should run it through to_python first to get a boolean value + return self.to_python(initial) != self.to_python(data) diff --git a/apps/common/forms.py b/apps/common/forms.py index 60cb1ae37..073bb671f 100644 --- a/apps/common/forms.py +++ b/apps/common/forms.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from django.db import transaction from .models import Setting +from .fields import DictField def to_model_value(value): @@ -18,7 +19,10 @@ def to_model_value(value): def to_form_value(value): try: - return json.loads(value) + data = json.loads(value) + if isinstance(data, dict): + data = value + return data except json.JSONDecodeError: return '' @@ -26,23 +30,26 @@ def to_form_value(value): 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) + 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") + settings = Setting.objects.all() 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 + if value == to_form_value(getattr(settings, name).value): + continue + defaults = { 'name': name, 'value': to_model_value(value) @@ -52,6 +59,24 @@ class BaseForm(forms.Form): raise ValueError(self.errors) +class BasicSettingForm(BaseForm): + SITE_URL = forms.URLField( + label=_("Current SITE URL"), + help_text="http://jumpserver.abc.com:8080" + ) + USER_GUIDE_URL = forms.URLField( + label=_("User Guide URL"), + help_text=_("User first login update profile done redirect to it") + ) + EMAIL_SUBJECT_PREFIX = forms.CharField( + max_length=1024, label=_("Email Subject Prefix"), + initial="[Jumpserver] " + ) + AUTH_LDAP = forms.BooleanField( + label=_("Enable LDAP Auth"), initial=False, required=False + ) + + class EmailSettingForm(BaseForm): EMAIL_HOST = forms.CharField( max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org' @@ -72,3 +97,35 @@ class EmailSettingForm(BaseForm): label=_("Use TLS"), initial=False, required=False, help_text=_("If SMTP port is 587, may be select") ) + + +class LDAPSettingForm(BaseForm): + AUTH_LDAP_SERVER_URI = forms.CharField( + label=_("LDAP server"), initial='ldap://localhost:389' + ) + AUTH_LDAP_BIND_DN = forms.CharField( + label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org' + ) + AUTH_LDAP_BIND_PASSWORD = forms.CharField( + label=_("Password"), initial='', + widget=forms.PasswordInput, required=False + ) + AUTH_LDAP_SEARCH_OU = forms.CharField( + label=_("User OU"), initial='ou=tech,dc=jumpserver,dc=org' + ) + AUTH_LDAP_SEARCH_FILTER = forms.CharField( + label=_("User search filter"), initial='(cn=%(user)s)' + ) + AUTH_LDAP_USER_ATTR_MAP = DictField( + label=_("User attr map"), + initial=json.dumps({ + "username": "cn", + "name": "sn", + "email": "mail" + }) + ) + # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU + # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER + AUTH_LDAP_START_TLS = forms.BooleanField( + label=_("Use SSL"), initial=False, required=False + ) diff --git a/apps/common/models.py b/apps/common/models.py index 3d6ee6008..091b8b83a 100644 --- a/apps/common/models.py +++ b/apps/common/models.py @@ -1,8 +1,10 @@ import json +import ldap from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings +from django_auth_ldap.config import LDAPSearch class SettingQuerySet(models.QuerySet): @@ -30,6 +32,13 @@ class Setting(models.Model): def __str__(self): return self.name + @property + def value_(self): + try: + return json.loads(self.value) + except json.JSONDecodeError: + return None + @classmethod def refresh_all_settings(cls): settings_list = cls.objects.all() @@ -43,5 +52,17 @@ class Setting(models.Model): return setattr(settings, self.name, value) + if self.name == "AUTH_LDAP": + if self.value_ and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: + settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) + elif not self.value_ and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: + settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) + + if self.name == "AUTH_LDAP_SEARCH_FILTER": + settings.AUTH_LDAP_USER_SEARCH = LDAPSearch( + settings.AUTH_LDAP_SEARCH_OU, ldap.SCOPE_SUBTREE, + settings.AUTH_LDAP_SEARCH_FILTER, + ) + class Meta: db_table = "settings" diff --git a/apps/common/serializers.py b/apps/common/serializers.py index 37e6555a3..9d389776d 100644 --- a/apps/common/serializers.py +++ b/apps/common/serializers.py @@ -8,3 +8,14 @@ class MailTestSerializer(serializers.Serializer): EMAIL_HOST_PASSWORD = serializers.CharField() EMAIL_USE_SSL = serializers.BooleanField(default=False) EMAIL_USE_TLS = serializers.BooleanField(default=False) + + +class LDAPTestSerializer(serializers.Serializer): + AUTH_LDAP_SERVER_URI = serializers.CharField(max_length=1024) + AUTH_LDAP_BIND_DN = serializers.CharField(max_length=1024) + AUTH_LDAP_BIND_PASSWORD = serializers.CharField() + AUTH_LDAP_SEARCH_OU = serializers.CharField() + AUTH_LDAP_SEARCH_FILTER = serializers.CharField() + AUTH_LDAP_USER_ATTR_MAP = serializers.CharField() + AUTH_LDAP_START_TLS = serializers.BooleanField(required=False) + diff --git a/apps/common/signals.py b/apps/common/signals.py index de8a84139..6edf140e2 100644 --- a/apps/common/signals.py +++ b/apps/common/signals.py @@ -4,3 +4,4 @@ from django.dispatch import Signal django_ready = Signal() +ldap_auth_enable = Signal(providing_args=["enabled"]) diff --git a/apps/common/signals_handler.py b/apps/common/signals_handler.py index 6b54706db..076ea7925 100644 --- a/apps/common/signals_handler.py +++ b/apps/common/signals_handler.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # - +import ldap from django.dispatch import receiver from django.db.models.signals import post_save +from django.conf import settings +from django_auth_ldap.config import LDAPSearch from .models import Setting from .utils import get_logger -from .signals import django_ready - +from .signals import django_ready, ldap_auth_enable logger = get_logger(__file__) @@ -25,3 +26,17 @@ def refresh_all_settings_on_django_ready(sender, **kwargs): logger.debug("Receive django ready signal") logger.debug(" - fresh all settings") Setting.refresh_all_settings() + + +@receiver(ldap_auth_enable, dispatch_uid="my_unique_identifier") +def ldap_auth_on_changed(sender, enabled=True, **kwargs): + if enabled: + logger.debug("Enable LDAP auth") + if settings.AUTH_LDAP_BACKEND not in settings.AUTH_LDAP_BACKEND: + settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) + + else: + logger.debug("Disable LDAP auth") + if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: + settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) + diff --git a/apps/common/templates/common/basic_setting.html b/apps/common/templates/common/basic_setting.html new file mode 100644 index 000000000..e24f1d6a8 --- /dev/null +++ b/apps/common/templates/common/basic_setting.html @@ -0,0 +1,101 @@ +{% extends 'base.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} +{% load common_tags %} + +{% block content %} +