diff --git a/modoboa/core/api/v2/serializers.py b/modoboa/core/api/v2/serializers.py index b1659e65d..f7cf5bdf6 100644 --- a/modoboa/core/api/v2/serializers.py +++ b/modoboa/core/api/v2/serializers.py @@ -39,7 +39,7 @@ class CoreGlobalParametersSerializer(serializers.Serializer): update_scheme = serializers.BooleanField(default=True) default_password = serializers.CharField(default="password") random_password_length = serializers.IntegerField(min_value=8, default=8) - update_password_url = serializers.URLField(required=False) + update_password_url = serializers.URLField(required=False, allow_blank=True) password_recovery_msg = serializers.CharField( required=False, allow_blank=True) sms_password_recovery = serializers.BooleanField(default=False) @@ -86,14 +86,16 @@ class CoreGlobalParametersSerializer(serializers.Serializer): default="", required=False, allow_blank=True) # LDAP sync settings - ldap_sync_bind_dn = serializers.CharField(required=False) - ldap_sync_bind_password = serializers.CharField(required=False) + ldap_sync_bind_dn = serializers.CharField(required=False, allow_blank=True) + ldap_sync_bind_password = serializers.CharField( + required=False, allow_blank=True) ldap_enable_sync = serializers.BooleanField(default=False) ldap_sync_delete_remote_account = serializers.BooleanField(default=False) ldap_sync_account_dn_template = serializers.CharField( required=False, allow_blank=True) ldap_enable_import = serializers.BooleanField(default=False) - ldap_import_search_base = serializers.CharField(required=False) + ldap_import_search_base = serializers.CharField( + required=False, allow_blank=True) ldap_import_search_filter = serializers.CharField( default="(cn=*)", required=False ) @@ -124,7 +126,14 @@ class CoreGlobalParametersSerializer(serializers.Serializer): log_maximum_age = serializers.IntegerField(default=365) items_per_page = serializers.IntegerField(default=30) default_top_redirection = serializers.ChoiceField( - default="user", choices=[]) + default="user", choices=[""]) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + sms_backend_fields = sms_backends.get_all_backend_serializer_settings() + for field, definition in sms_backend_fields.items(): + self.fields[field] = definition["type"]( + **definition["attrs"]) def validate_ldap_user_dn_template(self, value): try: @@ -175,15 +184,14 @@ class CoreGlobalParametersSerializer(serializers.Serializer): else: errors["sms_provider"] = _("This field is required") - if data["authentication_type"] != "ldap": - return data - if data["ldap_auth_method"] == "searchbind": - required_fields = ["ldap_search_base", "ldap_search_filter"] - else: - required_fields = ["ldap_user_dn_template"] - for f in required_fields: - if data.get(f, u"") == u"": - errors[f] = _("This field is required") + if data["authentication_type"] == "ldap": + if data["ldap_auth_method"] == "searchbind": + required_fields = ["ldap_search_base", "ldap_search_filter"] + else: + required_fields = ["ldap_user_dn_template"] + for f in required_fields: + if data.get(f, "") == "": + errors[f] = _("This field is required") if len(errors): raise serializers.ValidationError(errors) return data diff --git a/modoboa/core/api/v2/tests.py b/modoboa/core/api/v2/tests.py new file mode 100644 index 000000000..3c8db027f --- /dev/null +++ b/modoboa/core/api/v2/tests.py @@ -0,0 +1,108 @@ +"""Core API related tests.""" + +import copy + +from django.urls import reverse + +from modoboa.lib.tests import ModoAPITestCase + +CORE_SETTINGS = { + "authentication_type": "local", + "password_scheme": "sha512crypt", + "rounds_number": 70000, + "update_scheme": True, + "default_password": "Toto12345", + "random_password_length": 8, + "update_password_url": "", + "password_recovery_msg": "", + "sms_password_recovery": False, + "ldap_server_address": "localhost", + "ldap_server_port": 389, + "ldap_enable_secondary_server": False, + "ldap_secondary_server_address": "localhost", + "ldap_secondary_server_port": 389, + "ldap_secured": "none", + "ldap_is_active_directory": False, + "ldap_admin_groups": "", + "ldap_group_type": "posixgroup", + "ldap_groups_search_base": "", + "ldap_password_attribute": "userPassword", + "ldap_auth_method": "searchbind", + "ldap_bind_dn": "", + "ldap_bind_password": "", + "ldap_search_base": "", + "ldap_search_filter": "(mail=%(user)s)", + "ldap_user_dn_template": "", + "ldap_sync_bind_dn": "", + "ldap_sync_bind_password": "", + "ldap_enable_sync": False, + "ldap_sync_delete_remote_account": False, + "ldap_sync_account_dn_template": "", + "ldap_enable_import": False, + "ldap_import_search_base": "", + "ldap_import_search_filter": "(cn=*)", + "ldap_import_username_attr": "cn", + "ldap_dovecot_sync": False, + "ldap_dovecot_conf_file": "/etc/dovecot/dovecot-modoboa.conf", + "rss_feed_url": "", + "hide_features_widget": False, + "sender_address": "noreply@yourdomain.test", + "enable_api_communication": True, + "check_new_versions": True, + "send_new_versions_email": False, + "new_versions_email_rcpt": "postmaster@yourdomain.test", + "send_statistics": True, + "inactive_account_threshold": 30, + "top_notifications_check_interval": 30, + "log_maximum_age": 365, + "items_per_page": 30, + "default_top_redirection": "" +} + + +class ParametersAPITestCase(ModoAPITestCase): + + def test_update(self): + url = reverse("v2:parameter-detail", args=["core"]) + data = copy.copy(CORE_SETTINGS) + resp = self.client.put(url, data, format="json") + self.assertEqual(resp.status_code, 200) + + # Modify SMS related settings + data["sms_password_recovery"] = True + resp = self.client.put(url, data, format="json") + self.assertEqual(resp.status_code, 400) + self.assertIn("sms_provider", resp.json()) + data["sms_provider"] = "test" + resp = self.client.put(url, data, format="json") + self.assertEqual(resp.status_code, 400) + data.update({ + "sms_provider": "ovh", + "sms_ovh_application_key": "key", + "sms_ovh_application_secret": "secret", + "sms_ovh_consumer_key": "consumer" + }) + resp = self.client.put(url, data, format="json") + self.assertEqual(resp.status_code, 200) + + # Modify some LDAP settings + data.update({ + "authentication_type": "ldap", + "ldap_auth_method": "searchbind" + }) + resp = self.client.put(url, data, format="json") + self.assertEqual(resp.status_code, 400) + self.assertIn("ldap_search_base", resp.json()) + + data.update({"ldap_auth_method": "directbind"}) + resp = self.client.put(url, data, format="json") + self.assertEqual(resp.status_code, 400) + self.assertIn("ldap_user_dn_template", resp.json()) + + data.update({ + "ldap_user_dn_template": "%(user)s", + "ldap_sync_account_dn_template": "%(user)s", + "ldap_search_filter": "mail=%(user)s" + }) + resp = self.client.put(url, data, format="json") + self.assertEqual(resp.status_code, 200) diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index b43917392..a48d54045 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -784,6 +784,7 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ "Choose a provider to send password recovery SMS" ) }), + *sms_backends.get_all_backend_structures() ]) }), ("ldap", { diff --git a/modoboa/core/sms_backends/__init__.py b/modoboa/core/sms_backends/__init__.py index f3814a199..9ac1ee8a1 100644 --- a/modoboa/core/sms_backends/__init__.py +++ b/modoboa/core/sms_backends/__init__.py @@ -8,7 +8,9 @@ from .. import constants class SMSBackend: """Base class of every SMS backend.""" + serializer_settings = {} settings = {} + structure = [] visibility_rules = {} def __init__(self, params): @@ -54,6 +56,48 @@ def get_all_backend_settings(): return settings +def get_backend_serializer_settings(name): + """Return serializer settings of given backend.""" + backend_class = get_backend_class(name) + if not backend_class: + return None + return backend_class.serializer_settings + + +def get_all_backend_serializer_settings(): + """Return serializer settings of all backends.""" + settings = {} + for backend in constants.SMS_BACKENDS: + name = backend[0] + if not name: + continue + b_settings = get_backend_serializer_settings(name) + if b_settings: + settings.update(b_settings) + return settings + + +def get_backend_structure(name): + """Return parameters structure of given backend.""" + backend_class = get_backend_class(name) + if not backend_class: + return None + return backend_class.structure + + +def get_all_backend_structures(): + """Return parameters structure for all backends.""" + structure = [] + for backend in constants.SMS_BACKENDS: + name = backend[0] + if not name: + continue + b_structure = get_backend_structure(name) + if b_structure: + structure += b_structure + return structure + + def get_all_backend_visibility_rules(): """Return visibility rules of all backends.""" rules = {} diff --git a/modoboa/core/sms_backends/ovh.py b/modoboa/core/sms_backends/ovh.py index db40c9bc1..6c9af68d5 100644 --- a/modoboa/core/sms_backends/ovh.py +++ b/modoboa/core/sms_backends/ovh.py @@ -6,6 +6,8 @@ from django import forms from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + from . import SMSBackend @@ -54,6 +56,63 @@ class OVHBackend(SMSBackend): } } + serializer_settings = { + "sms_ovh_endpoint": { + "type": serializers.ChoiceField, + "attrs": { + "default": "ovh-eu", + "choices": ( + ("ovh-eu", _("OVH Europe")), + ("ovh-us", _("OVH US")), + ("ovh-ca", _("OVH North-America")), + ("soyoustart-eu", _("So you Start Europe")), + ("soyoustart-ca", _("So you Start North America")), + ("kimsufi-eu", _("Kimsufi Europe")), + ("kimsufi-ca", _("Kimsufi North America")), + ) + } + }, + "sms_ovh_application_key": { + "type": serializers.CharField, + "attrs": { + "required": False + } + }, + "sms_ovh_application_secret": { + "type": serializers.CharField, + "attrs": { + "required": False + } + }, + "sms_ovh_consumer_key": { + "type": serializers.CharField, + "attrs": { + "required": False + } + } + } + + structure = [ + ("sms_ovh_endpoint", { + "label": _("API endpoint"), + "display": "sms_password_recovery=true&sms_provider=ovh", + }), + ("sms_ovh_application_key", { + "label": _("Application key"), + "display": "sms_password_recovery=true&sms_provider=ovh" + }), + ("sms_ovh_application_secret", { + "label": _("Appication secret"), + "display": "sms_password_recovery=true&sms_provider=ovh", + "password": True + }), + ("sms_ovh_consumer_key", { + "label": _("Consumer key"), + "display": "sms_password_recovery=true&sms_provider=ovh", + "password": True + }), + ] + visibility_rules = { "sms_ovh_endpoint": "sms_provider=ovh", "sms_ovh_application_key": "sms_provider=ovh", diff --git a/modoboa/parameters/api/v2/tests.py b/modoboa/parameters/api/v2/tests.py new file mode 100644 index 000000000..0d3dbba7f --- /dev/null +++ b/modoboa/parameters/api/v2/tests.py @@ -0,0 +1,50 @@ +"""Parameters API related tests.""" + +from django.urls import reverse + +from modoboa.lib.tests import ModoAPITestCase + + +class ParametersAPITestCase(ModoAPITestCase): + + def test_applications(self): + url = reverse("v2:parameter-applications") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + # core, admin, limits + self.assertEqual(len(resp.json()), 3) + + def test_get_structure(self): + url = reverse("v2:parameter-structure") + resp_full = self.client.get(url) + self.assertEqual(resp_full.status_code, 200) + + resp_core = self.client.get(url + "?app=core") + self.assertEqual(resp_core.status_code, 200) + + self.assertNotEqual(len(resp_full.json()), len(resp_core.json())) + + def test_retrieve(self): + for app in ["core", "admin", "limits"]: + url = reverse("v2:parameter-detail", args=[app]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + + def test_update(self): + data = { + "enable_admin_limits": True, + "deflt_user_domain_admins_limit": 0, + "deflt_user_domains_limit": 0, + "deflt_user_domain_aliases_limit": 0, + "deflt_user_mailboxes_limit": 0, + "deflt_user_mailbox_aliases_limit": 0, + "deflt_user_quota_limit": 0, + "enable_domain_limits": False, + "deflt_domain_domain_admins_limit": 0, + "deflt_domain_domain_aliases_limit": 0, + "deflt_domain_mailboxes_limit": 0, + "deflt_domain_mailbox_aliases_limit": 0 + } + url = reverse("v2:parameter-detail", args=["limits"]) + resp = self.client.put(url, json=data) + self.assertEqual(resp.status_code, 200)