diff --git a/frontend/src/api.js b/frontend/src/api.js index 379c6a52f..e113a8109 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -16,3 +16,16 @@ export const createDomain = data => { export const updateDomain = data => { return domainResource.update({ pk: data.pk }, data) } + +var customParametersActions = { + structure: {method: 'GET', url: '/api/v1/parameters/structure/'} +} +var parametersResource = Vue.resource('/api/v1/parameters/', {}, customParametersActions) + +export const getParameters = () => { + return parametersResource.get() +} + +export const getParametersStructure = () => { + return parametersResource.structure() +} diff --git a/frontend/src/components/navigation/Navbar.vue b/frontend/src/components/navigation/Navbar.vue index 0ef5777fb..5ccf79e2e 100644 --- a/frontend/src/components/navigation/Navbar.vue +++ b/frontend/src/components/navigation/Navbar.vue @@ -59,9 +59,9 @@ export default { label: 'Logs' }, { - route: 'DomainList', + route: 'ParametersEdit', icon: 'tune', - label: 'Settings' + label: 'Parameters' } ] } diff --git a/frontend/src/components/parameters/ParametersForm.vue b/frontend/src/components/parameters/ParametersForm.vue new file mode 100644 index 000000000..dd0d2f3e3 --- /dev/null +++ b/frontend/src/components/parameters/ParametersForm.vue @@ -0,0 +1,46 @@ + + + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 27fcb93c6..618dd0931 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -2,25 +2,31 @@ import Vue from 'vue' import Router from 'vue-router' import DomainList from '@/components/domains/DomainList' import DomainForm from '@/components/domains/DomainForm' +import ParametersForm from '@/components/parameters/ParametersForm' Vue.use(Router) export default new Router({ - routes: [ - { - path: '/domains/', - name: 'DomainList', - component: DomainList - }, - { - path: '/domains/add/', - name: 'DomainAdd', - component: DomainForm - }, - { - path: '/domains/:domainPk([0-9]+)/', - name: 'DomainEdit', - component: DomainForm - } - ] + routes: [ + { + path: '/domains/', + name: 'DomainList', + component: DomainList + }, + { + path: '/domains/add/', + name: 'DomainAdd', + component: DomainForm + }, + { + path: '/domains/:domainPk([0-9]+)/', + name: 'DomainEdit', + component: DomainForm + }, + { + path: '/parameters/', + name: 'ParametersEdit', + component: ParametersForm + } + ] }) diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index c0af12596..4eafc5716 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -7,6 +7,8 @@ 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 @@ -441,3 +443,180 @@ class GeneralParametersForm(param_forms.AdminParametersForm): }) else: settings.AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_REFERRALS] = False + + +GLOBAL_PARAMETERS = { + "authentication": { + "label": ugettext_lazy("Authentication"), + "params": { + "authentication_type": { + "field": serializers.ChoiceField( + choices=[("local", ugettext_lazy("Local")), + ("ldap", "LDAP")], + default="local" + ), + "label": ugettext_lazy("Authentication type"), + "help_text": ugettext_lazy( + "The backend used for authentication"), + }, + "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": { + "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": { + "label": ugettext_lazy("Default password"), + "field": serializers.CharField(default="password"), + "help_text": ugettext_lazy( + "Default password for automatically created accounts.") + }, + "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": { + "label": _("LDAP"), + "display": "authentication_type=ldap", + "params": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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 3e4fa2859..2e1b1b17b 100644 --- a/modoboa/core/apps.py +++ b/modoboa/core/apps.py @@ -16,10 +16,13 @@ def load_core_settings(): order to load base settings. """ from modoboa.parameters import tools as param_tools - from .app_settings import GeneralParametersForm + from . import app_settings param_tools.registry.add( - "global", GeneralParametersForm, ugettext_lazy("General")) + "global", app_settings.GeneralParametersForm, ugettext_lazy("General")) + param_tools.registry.add2( + "global", "core", ugettext_lazy("General"), + app_settings.GLOBAL_PARAMETERS) class CoreConfig(AppConfig): diff --git a/modoboa/core/constants.py b/modoboa/core/constants.py index 997198672..03db30206 100644 --- a/modoboa/core/constants.py +++ b/modoboa/core/constants.py @@ -55,6 +55,11 @@ LDAP_SECURE_MODES = [ ("ssl", "SSL/TLS") ] +LDAP_AUTH_METHODS = [ + ("searchbind", ugettext_lazy("Search and bind")), + ("directbind", ugettext_lazy("Direct bind")) +] + PERMISSIONS = { "SimpleUsers": [], "DomainAdmins": [ diff --git a/modoboa/parameters/serializers.py b/modoboa/parameters/serializers.py new file mode 100644 index 000000000..42d5f4514 --- /dev/null +++ b/modoboa/parameters/serializers.py @@ -0,0 +1,16 @@ +"""Parameters serializers.""" + +from rest_framework import serializers + +from . import tools + + +class ParametersSerializer(serializers.Serializer): + """Parameters serializer.""" + + def __init__(self, *args, **kwargs): + """Generate serializer.""" + super(ParametersSerializer, self).__init__(*args, **kwargs) + for name, field in tools.registry.get_fields("global"): + field.source = None + self.fields[name] = field diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py index 5187ec0a5..5b0ac2983 100644 --- a/modoboa/parameters/tools.py +++ b/modoboa/parameters/tools.py @@ -35,6 +35,10 @@ class Registry(object): "global": {}, "user": {} } + self._registry2 = { + "global": {}, + "user": {} + } def add(self, level, formclass, label): """Add a new class containing parameters.""" @@ -42,6 +46,12 @@ class Registry(object): "label": label, "formclass": formclass, "defaults": {} } + def add2(self, level, app, label, params): + """Add new parameters for given app and level.""" + self._registry2[level][app] = { + "label": label, "params": params + } + def _load_default_values(self, level): """Load default values.""" for _app, data in list(self._registry[level].items()): @@ -76,6 +86,39 @@ class Registry(object): }) return result + def get_fields(self, level, *args, **kwargs): + """Return fields to include in serializer.""" + result = [] + for app, fields in list(self._registry2[level].items()): + for section, sconfig in list(fields["params"].items()): + for name, config in list(sconfig["params"].items()): + result.append(("{}-{}".format(app, name), config["field"])) + return result + + def get_structure(self, level): + """Return fields structure.""" + result = [] + for app, fields in list(self._registry2[level].items()): + for section, sconfig in list(fields["params"].items()): + result.append({ + "type": "section", + "label": sconfig["label"], + "display": sconfig.get("display", "") + }) + for name, config in list(sconfig["params"].items()): + data = { + "type": "field", + "name": "{}-{}".format(app, name), + "label": config["label"], + "help_text": config.get("help_text", ""), + "display": config.get("display", ""), + "widget": config["field"].__class__.__name__, + } + if data["widget"] == "ChoiceField": + data["choices"] = config["field"].choices + result.append(data) + return result + def exists(self, level, app, parameter=None): """Check if parameter exists.""" parameters = self._registry[level] @@ -137,6 +180,15 @@ class Manager(object): value = self._parameters[app].get(parameter, value) yield (parameter, value) + def get_values_dict(self, app=None): + """Return all values for the given app as a dictionary.""" + 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()) + } + def set_value(self, parameter, value, app=None): """Set parameter for the given app.""" if app is None: diff --git a/modoboa/parameters/urls_api.py b/modoboa/parameters/urls_api.py new file mode 100644 index 000000000..e9cbeabb3 --- /dev/null +++ b/modoboa/parameters/urls_api.py @@ -0,0 +1,11 @@ +"""Parameters API urls.""" + +from rest_framework import routers + +from . import viewsets + + +router = routers.SimpleRouter() +router.register( + r"parameters", viewsets.ParametersViewSet, base_name="parameter") +urlpatterns = router.urls diff --git a/modoboa/parameters/viewsets.py b/modoboa/parameters/viewsets.py new file mode 100644 index 000000000..ae90feed9 --- /dev/null +++ b/modoboa/parameters/viewsets.py @@ -0,0 +1,23 @@ +"""Parameters viewsets.""" + +from rest_framework import decorators, response, viewsets + +from . import serializers +from . import tools + + +class ParametersViewSet(viewsets.ViewSet): + """Parameter viewset.""" + + @decorators.list_route(methods=["get"]) + def structure(self, request): + """Return parameter schema.""" + data = tools.registry.get_structure("global") + return response.Response(data) + + def list(self, request): + """Return all parameters.""" + parameters = request.localconfig.parameters.get_values_dict("core") + serializer = serializers.ParametersSerializer(data=parameters) + serializer.is_valid(raise_exception=True) + return response.Response(serializer.validated_data) diff --git a/modoboa/urls_api.py b/modoboa/urls_api.py index ea1770cda..ca24ad5da 100644 --- a/modoboa/urls_api.py +++ b/modoboa/urls_api.py @@ -10,6 +10,7 @@ from modoboa.core.extensions import exts_pool urlpatterns = [ url("", include("modoboa.admin.urls_api")), + url("", include("modoboa.parameters.urls_api")), url("", include("modoboa.limits.urls_api")), url("", include("modoboa.relaydomains.urls_api")), ]