From 19f2af429a59285450e36602a03f403e0579b98e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 29 Aug 2018 18:21:16 +0200 Subject: [PATCH] Better parameters management. --- frontend/src/api.js | 20 ++- frontend/src/components/navigation/Navbar.vue | 23 ++- .../components/parameters/ParametersForm.vue | 103 +++++++++---- frontend/src/router/index.js | 2 +- modoboa/core/app_settings.py | 143 ++++++------------ modoboa/core/apps.py | 4 +- modoboa/core/serializers.py | 104 +++++++++++++ modoboa/parameters/serializers.py | 3 +- modoboa/parameters/tools.py | 44 ++++-- modoboa/parameters/viewsets.py | 32 +++- 10 files changed, 321 insertions(+), 157 deletions(-) create mode 100644 modoboa/core/serializers.py diff --git a/frontend/src/api.js b/frontend/src/api.js index 173ddd351..bdc51d053 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -18,14 +18,24 @@ export const updateDomain = data => { } var customParametersActions = { + applications: {method: 'GET', url: '/api/v1/parameters/applications/'}, structure: {method: 'GET', url: '/api/v1/parameters/structure/'} } -var parametersResource = Vue.resource('/api/v1/parameters/', {}, customParametersActions) +var parametersResource = Vue.resource( + '/api/v1/parameters{/app}/', {}, customParametersActions) -export const getParameters = () => { - return parametersResource.get() +export const getParametersForApplication = (app) => { + return parametersResource.get({app: app}) } -export const getParametersStructure = () => { - return parametersResource.structure() +export const getParametersApplications = () => { + return parametersResource.applications() +} + +export const getParametersStructure = (app) => { + return parametersResource.structure({app: app}) +} + +export const saveParametersForApplication = (app, values) => { + return parametersResource.update({app: app}, values) } diff --git a/frontend/src/components/navigation/Navbar.vue b/frontend/src/components/navigation/Navbar.vue index b2b9ec57b..646bccd9e 100644 --- a/frontend/src/components/navigation/Navbar.vue +++ b/frontend/src/components/navigation/Navbar.vue @@ -25,8 +25,8 @@ v-else-if="item.children" v-model="item.model" :key="item.text" - :prepend-icon="item.model ? item.icon : item['icon-alt']" - append-icon="" + :prepend-icon="item.icon" + :append-icon="item['icon-alt']" > @@ -66,6 +66,8 @@ diff --git a/frontend/src/components/parameters/ParametersForm.vue b/frontend/src/components/parameters/ParametersForm.vue index 3ac4a4315..d5cc3dbc3 100644 --- a/frontend/src/components/parameters/ParametersForm.vue +++ b/frontend/src/components/parameters/ParametersForm.vue @@ -1,36 +1,57 @@ @@ -41,17 +62,37 @@ import * as api from '@/api' export default { data () { return { + active: 0, structure: [], - parameters: {} + parameters: {}, + formErrors: {} } }, created () { - api.getParametersStructure().then(response => { + var app = this.$route.params.app + api.getParametersStructure(app).then(response => { this.structure = response.data }) - api.getParameters().then(response => { + api.getParametersForApplication(app).then(response => { this.parameters = response.data }) + }, + methods: { + display (element) { + if (element.display === '') { + return true + } + var [field, value] = element.display.split('=') + return this.parameters[field] === value + }, + save () { + api.saveParametersForApplication( + this.$route.params.app, this.parameters + ).then(response => { + }, response => { + this.formErrors = response.data + }) + } } } diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 618dd0931..8aee91762 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -24,7 +24,7 @@ export default new Router({ component: DomainForm }, { - path: '/parameters/', + path: '/parameters/:app', name: 'ParametersEdit', component: ParametersForm } diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 4eafc5716..330078578 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -2,13 +2,13 @@ from __future__ import unicode_literals +import collections + from django import forms from django.conf import settings from django.contrib.auth import password_validation from django.utils.translation import ugettext as _, ugettext_lazy -from rest_framework import serializers - from modoboa.lib import fields as lib_fields from modoboa.lib.form_utils import ( HorizontalRadioSelect, SeparatorField, YesNoField @@ -445,178 +445,135 @@ class GeneralParametersForm(param_forms.AdminParametersForm): settings.AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_REFERRALS] = False -GLOBAL_PARAMETERS = { - "authentication": { +GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ + ("authentication", { "label": ugettext_lazy("Authentication"), - "params": { - "authentication_type": { - "field": serializers.ChoiceField( - choices=[("local", ugettext_lazy("Local")), - ("ldap", "LDAP")], - default="local" - ), + "params": collections.OrderedDict([ + ("authentication_type", { "label": ugettext_lazy("Authentication type"), "help_text": ugettext_lazy( "The backend used for authentication"), - }, - "password_scheme": { + }), + ("password_scheme", { "label": ugettext_lazy("Default password scheme"), - "field": serializers.ChoiceField( - choices=[("sha512crypt", "sha512crypt"), - ("sha256crypt", "sha256crypt"), - ("blfcrypt", "bcrypt"), - ("md5crypt", ugettext_lazy("md5crypt (weak)")), - ("sha256", ugettext_lazy("sha256 (weak)")), - ("md5", ugettext_lazy("md5 (weak)")), - ("crypt", ugettext_lazy("crypt (weak)")), - ("plain", ugettext_lazy("plain (weak)"))], - default="sha512crypt" - ), "help_text": ugettext_lazy( "Scheme used to crypt mailbox passwords"), - }, - "rounds_number": { + }), + ("rounds_number", { "label": ugettext_lazy("Rounds"), - "field": serializers.IntegerField(default=70000), "help_text": ugettext_lazy( "Number of rounds to use (only used by sha256crypt and " "sha512crypt). Must be between 1000 and 999999999, " "inclusive." ) - }, - "default_password": { + }), + ("default_password", { "label": ugettext_lazy("Default password"), - "field": serializers.CharField(default="password"), "help_text": ugettext_lazy( "Default password for automatically created accounts.") - }, - "random_password_length": { + }), + ("random_password_length", { "label": ugettext_lazy("Random password length"), - "field": serializers.IntegerField(min_value=8, default=8), "help_text": ugettext_lazy( "Length of randomly generated passwords.") - } - } - }, - "ldap": { + }) + ]) + }), + ("ldap", { "label": _("LDAP"), "display": "authentication_type=ldap", - "params": { - "ldap_server_address": { + "params": collections.OrderedDict([ + ("ldap_server_address", { "label": ugettext_lazy("Server address"), - "field": serializers.CharField(default="localhost"), "help_text": ugettext_lazy( "The IP address or the DNS name of the LDAP server"), - }, - "ldap_server_port": { + }), + ("ldap_server_port", { "label": ugettext_lazy("Server port"), - "field": serializers.IntegerField(default=389), "help_text": ugettext_lazy( "The TCP port number used by the LDAP server") - }, - "ldap_secured": { + }), + ("ldap_secured", { "label": ugettext_lazy("Use a secured connection"), - "field": serializers.ChoiceField( - choices=constants.LDAP_SECURE_MODES, - default="none" - ), "help_text": ugettext_lazy( "Use an SSL/STARTTLS connection to access the LDAP server") - }, - "ldap_auth_method": { + }), + ("ldap_auth_method", { "label": ugettext_lazy("Authentication method"), - "field": serializers.ChoiceField( - choices=constants.LDAP_AUTH_METHODS, - default="searchbind", - ), "help_text": ugettext_lazy( "Choose the authentication method to use"), - }, - "ldap_bind_dn": { + }), + ("ldap_bind_dn", { "label": ugettext_lazy("Bind DN"), - "field": serializers.CharField(default="", required=False), "help_text": ugettext_lazy( "The distinguished name to use when binding to the LDAP " "server. Leave empty for an anonymous bind" ), "display": "ldap_auth_method=searchbind" - }, - "ldap_bind_password": { + }), + ("ldap_bind_password", { "label": ugettext_lazy("Bind password"), - "field": serializers.CharField(default="", required=False), "help_text": ugettext_lazy( "The password to use when binding to the LDAP server " "(with 'Bind DN')" ), "display": "ldap_auth_method=searchbind" - }, - "ldap_search_base": { + }), + ("ldap_search_base", { "label": ugettext_lazy("Users search base"), - "field": serializers.CharField(default="", required=False), "help_text": ugettext_lazy( "The distinguished name of the search base used to find " "users" ), "display": "ldap_auth_method=searchbind" - }, - "ldap_search_filter": { + }), + ("ldap_search_filter", { "label": ugettext_lazy("Search filter"), - "field": serializers.CharField( - default="(mail=%(user)s)", required=False), "help_text": ugettext_lazy( "An optional filter string (e.g. '(objectClass=person)'). " "In order to be valid, it must be enclosed in parentheses." ), "display": "ldap_auth_method=searchbind" - }, - "ldap_user_dn_template": { + }), + ("ldap_user_dn_template", { "label": ugettext_lazy("User DN template"), - "field": serializers.CharField(default="", required=False), "help_text": ugettext_lazy( "The template used to construct a user's DN. It should " "contain one placeholder (ie. %(user)s)" ), "display": "ldap_auth_method=directbind" - }, - "ldap_password_attribute": { + }), + ("ldap_password_attribute", { "label": ugettext_lazy("Password attribute"), - "field": serializers.CharField(default="userPassword"), "help_text": ugettext_lazy( "The attribute used to store user passwords"), - }, - "ldap_is_active_directory": { + }), + ("ldap_is_active_directory", { "label": ugettext_lazy("Active Directory"), - "field": serializers.BooleanField(default=False), "help_text": ugettext_lazy( "Tell if the LDAP server is an Active Directory one") - }, - "ldap_admin_groups": { + }), + ("ldap_admin_groups", { "label": ugettext_lazy("Administrator groups"), - "field": serializers.CharField(default="", required=False), "help_text": ugettext_lazy( "Members of those LDAP Posix groups will be created as " "domain administrators. Use ';' characters to separate " "groups." ) - }, - "ldap_group_type": { + }), + ("ldap_group_type", { "label": ugettext_lazy("Group type"), - "field": serializers.ChoiceField( - default="posixgroup", - choices=constants.LDAP_GROUP_TYPES - ), "help_text": ugettext_lazy( "The LDAP group type to use with your directory." ) - }, - "ldap_groups_search_base": { + }), + ("ldap_groups_search_base", { "label": ugettext_lazy("Groups search base"), - "field": serializers.CharField(default="", required=False), "help_text": ugettext_lazy( "The distinguished name of the search base used to find " "groups" ) - } - } - } -} + }) + ]) + }) +]) diff --git a/modoboa/core/apps.py b/modoboa/core/apps.py index 2e1b1b17b..74b62bca6 100644 --- a/modoboa/core/apps.py +++ b/modoboa/core/apps.py @@ -17,12 +17,14 @@ def load_core_settings(): """ from modoboa.parameters import tools as param_tools from . import app_settings + from . import serializers param_tools.registry.add( "global", app_settings.GeneralParametersForm, ugettext_lazy("General")) param_tools.registry.add2( "global", "core", ugettext_lazy("General"), - app_settings.GLOBAL_PARAMETERS) + app_settings.GLOBAL_PARAMETERS_STRUCT, + serializers.CoreGlobalParametersSerializer) class CoreConfig(AppConfig): diff --git a/modoboa/core/serializers.py b/modoboa/core/serializers.py new file mode 100644 index 000000000..98f0f73be --- /dev/null +++ b/modoboa/core/serializers.py @@ -0,0 +1,104 @@ +"""Core serializers.""" + +from django.utils.translation import ugettext_lazy, ugettext as _ + +from django.contrib.auth import password_validation + +from rest_framework import serializers + +from . import constants + + +class CoreGlobalParametersSerializer(serializers.Serializer): + """A serializer for global parameters.""" + + # General settings + authentication_type = serializers.ChoiceField( + choices=[("local", ugettext_lazy("Local")), + ("ldap", "LDAP")], + default="local" + ) + password_scheme = serializers.ChoiceField( + choices=[("sha512crypt", "sha512crypt"), + ("sha256crypt", "sha256crypt"), + ("blfcrypt", "bcrypt"), + ("md5crypt", ugettext_lazy("md5crypt (weak)")), + ("sha256", ugettext_lazy("sha256 (weak)")), + ("md5", ugettext_lazy("md5 (weak)")), + ("crypt", ugettext_lazy("crypt (weak)")), + ("plain", ugettext_lazy("plain (weak)"))], + default="sha512crypt" + ) + rounds_number = serializers.IntegerField(default=70000) + default_password = serializers.CharField(default="password") + random_password_length = serializers.IntegerField(min_value=8, default=8) + + # LDAP settings + ldap_server_address = serializers.CharField(default="localhost") + ldap_server_port = serializers.IntegerField(default=389) + ldap_secured = serializers.ChoiceField( + choices=constants.LDAP_SECURE_MODES, + default="none" + ) + ldap_auth_method = serializers.ChoiceField( + choices=constants.LDAP_AUTH_METHODS, + default="searchbind", + ) + ldap_bind_dn = serializers.CharField( + default="", required=False, allow_blank=True) + ldap_bind_password = serializers.CharField( + default="", required=False, allow_blank=True) + ldap_search_base = serializers.CharField( + default="", required=False, allow_blank=True) + ldap_search_filter = serializers.CharField( + default="(mail=%(user)s)", required=False, + allow_blank=True) + ldap_user_dn_template = serializers.CharField( + default="", required=False, allow_blank=True) + ldap_password_attribute = serializers.CharField(default="userPassword") + ldap_is_active_directory = serializers.BooleanField(default=False) + ldap_admin_groups = serializers.CharField( + default="", required=False, allow_blank=True) + ldap_group_type = serializers.ChoiceField( + default="posixgroup", + choices=constants.LDAP_GROUP_TYPES + ) + ldap_groups_search_base = serializers.CharField( + default="", required=False, allow_blank=True) + + def validate_ldap_user_dn_template(self, value): + try: + value % {"user": "toto"} + except (KeyError, ValueError): + raise serializers.ValidationError(_("Invalid syntax")) + return value + + def validate_rounds_number(self, value): + if value < 1000 or value > 999999999: + raise serializers.ValidationError(_("Invalid rounds number")) + return value + + def validate_default_password(self, value): + """Check password complexity.""" + password_validation.validate_password(value) + return value + + def validate(self, data): + """Custom validation method + + Depending on 'ldap_auth_method' value, we check for different + required parameters. + """ + 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"] + errors = {} + for f in required_fields: + if data.get(f, u"") == u"": + errors[f] = _("This field is required") + if len(errors): + raise serializers.ValidationError(errors) + return data diff --git a/modoboa/parameters/serializers.py b/modoboa/parameters/serializers.py index 42d5f4514..6594776f3 100644 --- a/modoboa/parameters/serializers.py +++ b/modoboa/parameters/serializers.py @@ -10,7 +10,8 @@ class ParametersSerializer(serializers.Serializer): def __init__(self, *args, **kwargs): """Generate serializer.""" + app = kwargs.pop("app") super(ParametersSerializer, self).__init__(*args, **kwargs) - for name, field in tools.registry.get_fields("global"): + for name, field in tools.registry.get_fields("global", app): field.source = None self.fields[name] = field diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py index f3a672e1e..bf7a54125 100644 --- a/modoboa/parameters/tools.py +++ b/modoboa/parameters/tools.py @@ -46,10 +46,11 @@ class Registry(object): "label": label, "formclass": formclass, "defaults": {} } - def add2(self, level, app, label, params): + def add2(self, level, app, label, structure, serializer_class): """Add new parameters for given app and level.""" self._registry2[level][app] = { - "label": label, "params": params + "label": label, "structure": structure, + "serializer_class": serializer_class } def _load_default_values(self, level): @@ -61,6 +62,12 @@ class Registry(object): continue data["defaults"][name] = field.initial + def get_applications(self, level): + """Return all applications registered for level.""" + result = [{"name": key, "label": value["label"]} + for key, value in self._registry2[level].items()] + return result + def get_forms(self, level, *args, **kwargs): """Return form instances for all app of the given level.""" sorted_apps = [] @@ -86,37 +93,49 @@ class Registry(object): }) return result - def get_fields(self, level, *args, **kwargs): + def get_fields(self, level, for_app=None): """Return fields to include in serializer.""" result = [] for app, fields in list(self._registry2[level].items()): - for section, sconfig in list(fields["params"].items()): + if app and for_app != app: + continue + for section, sconfig in list(fields["structure"].items()): for name, config in list(sconfig["params"].items()): result.append(("{}-{}".format(app, name), config["field"])) return result - def get_structure(self, level): + def get_serializer_class(self, level, app): + """Return serializer class for given level and app.""" + if app not in self._registry2[level]: + raise NotDefined(app) + return self._registry2[level][app]["serializer_class"] + + def get_structure(self, level, for_app=None): """Return fields structure.""" result = [] for app, fields in list(self._registry2[level].items()): - for section, sconfig in list(fields["params"].items()): + if for_app and for_app != app: + continue + serializer = fields["serializer_class"]() + for section, sconfig in list(fields["structure"].items()): item = { "label": sconfig["label"], "display": sconfig.get("display", ""), "parameters": [] } for name, config in list(sconfig["params"].items()): + field = serializer.fields[name] data = { - "name": "{}-{}".format(app, name), + "name": name, "label": config["label"], "help_text": config.get("help_text", ""), "display": config.get("display", ""), - "widget": config["field"].__class__.__name__, + "widget": field.__class__.__name__, } if data["widget"] == "ChoiceField": data["choices"] = [ {"text": value, "value": key} - for key, value in config["field"].choices.items() + for key, value in field.choices.items() ] item["parameters"].append(data) result.append(item) @@ -188,9 +207,10 @@ class Manager(object): if app is None: app = guess_extension_name() values = registry.get_defaults(self._level, app) - return { - parameter: value for parameter, value in list(values.items()) - } + for parameter, value in list(values.items()): + if app in self._parameters: + values[parameter] = self._parameters[app].get(parameter, value) + return values def set_value(self, parameter, value, app=None): """Set parameter for the given app.""" diff --git a/modoboa/parameters/viewsets.py b/modoboa/parameters/viewsets.py index ae90feed9..ffb80c13f 100644 --- a/modoboa/parameters/viewsets.py +++ b/modoboa/parameters/viewsets.py @@ -2,22 +2,40 @@ from rest_framework import decorators, response, viewsets -from . import serializers from . import tools class ParametersViewSet(viewsets.ViewSet): """Parameter viewset.""" + lookup_value_regex = r"\w+" + + @decorators.list_route(methods=["get"]) + def applications(self, request): + """Return application list.""" + applications = tools.registry.get_applications("global") + return response.Response(applications) + @decorators.list_route(methods=["get"]) def structure(self, request): """Return parameter schema.""" - data = tools.registry.get_structure("global") + app = request.GET.get("app") + data = tools.registry.get_structure("global", app) return response.Response(data) - def list(self, request): - """Return all parameters.""" - parameters = request.localconfig.parameters.get_values_dict("core") - serializer = serializers.ParametersSerializer(data=parameters) + def retrieve(self, request, pk): + """Return all parameters for given app.""" + parameters = request.localconfig.parameters.get_values_dict(pk) + serializer = tools.registry.get_serializer_class("global", pk)( + parameters) + return response.Response(serializer.data) + + def update(self, request, pk): + """Save parameters for given app.""" + serializer = tools.registry.get_serializer_class("global", pk)( + data=request.data) serializer.is_valid(raise_exception=True) - return response.Response(serializer.validated_data) + request.localconfig.parameters.set_values( + serializer.validated_data, app=pk) + request.localconfig.save(update_fields=["_parameters"]) + return response.Response()