Better parameters management.

This commit is contained in:
Antoine Nguyen
2018-10-05 14:06:59 +02:00
parent b41a096458
commit 19f2af429a
10 changed files with 321 additions and 157 deletions
+15 -5
View File
@@ -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)
}
+17 -6
View File
@@ -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']"
>
<v-list-tile slot="activator">
<v-list-tile-content>
@@ -66,6 +66,8 @@
</template>
<script>
import * as api from '@/api'
export default {
data () {
return {
@@ -78,8 +80,7 @@ export default {
{
text: 'Domains',
to: { name: 'DomainList' },
icon: 'domain',
model: true
icon: 'domain'
},
{
text: 'Identities',
@@ -90,12 +91,22 @@ export default {
text: 'Logs'
},
{
to: { name: 'ParametersEdit' },
icon: 'settings',
text: 'Parameters'
text: 'Parameters',
children: []
}
]
}
},
created () {
api.getParametersApplications().then(response => {
response.data.forEach(item => {
this.items[4].children.push({
text: item.label,
to: { name: 'ParametersEdit', params: {app: item.name} }
})
})
})
}
}
</script>
@@ -1,36 +1,57 @@
<template>
<v-layout>
<v-layout row wrap>
<v-flex>
<h1 class="title">Parameters</h1>
<v-expansion-panel>
<v-expansion-panel-content
v-for="(element, index) in structure"
:key="index"
<h1>Parameters</h1>
<v-tabs v-model="active">
<v-tab v-for="(element, index) in structure"
:key="index"
v-if="display(element)"
>
<div slot="header">{{ element.label }}</div>
<v-card>
<v-card-text>
<v-text-field v-for="(param, index) in element.parameters"
:label="param.label"
:key="index"
:hint="param.help_text"
{{ element.label }}
</v-tab>
<v-tabs-items>
<v-tab-item
v-for="(element, index) in structure"
:key="index"
v-if="display(element)"
>
<v-card>
<v-card-text>
<v-text-field v-for="(param, index) in element.parameters"
:label="param.label"
:key="index"
:hint="param.help_text"
v-model="parameters[param.name]"
:error="formErrors[param.name] !== undefined"
:error-messages="formErrors[param.name]"
v-if="(param.widget === 'CharField' || param.widget === 'IntegerField') && display(param)">
</v-text-field>
<v-checkbox :label="param.label"
v-model="parameters[param.name]"
v-if="param.widget === 'CharField' || param.widget === 'IntegerField'">
</v-text-field>
<v-checkbox :label="param.label"
v-else-if="param.widget === 'BooleanField' && display(param)"
>
</v-checkbox>
<v-select :label="param.label"
v-model="parameters[param.name]"
v-else-if="param.widget === 'BooleanField'"
>
</v-checkbox>
<v-select :label="param.label"
v-model="parameters[param.name]"
:items="param.choices"
v-else-if="param.widget === 'ChoiceField'">
</v-select>
</v-card-text>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
:items="param.choices"
v-else-if="param.widget === 'ChoiceField' && display(param)">
</v-select>
</v-card-text>
</v-card>
</v-tab-item>
</v-tabs-items>
</v-tabs>
<v-btn fixed
dark
fab
bottom
right
color="green"
@click="save"
>
<v-icon>save</v-icon>
</v-btn>
</v-flex>
</v-layout>
</template>
@@ -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
})
}
}
}
</script>
+1 -1
View File
@@ -24,7 +24,7 @@ export default new Router({
component: DomainForm
},
{
path: '/parameters/',
path: '/parameters/:app',
name: 'ParametersEdit',
component: ParametersForm
}
+50 -93
View File
@@ -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"
)
}
}
}
}
})
])
})
])
+3 -1
View File
@@ -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):
+104
View File
@@ -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
+2 -1
View File
@@ -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
+32 -12
View File
@@ -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."""
+25 -7
View File
@@ -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()