mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-20 06:08:35 +02:00
Merge pull request #1352 from modoboa/feature/per_domain_dkim_key
Per domain DKIM key generation.
This commit is contained in:
@@ -103,6 +103,20 @@ The *admin* application exposes several parameters, they are presented below:
|
||||
| | | | |
|
||||
| | | | |
|
||||
+--------------------+---------------------+-------------------------------+--------------------+
|
||||
|DKIM keys storage |Admin |Path to a directory where DKIM | |
|
||||
|directory | | generated keys will be stored | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
+--------------------+---------------------+-------------------------------+--------------------+
|
||||
|Default DKIM key |Admin |The default size (in bits) for |2048 |
|
||||
|length | |new keys | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
+--------------------+---------------------+-------------------------------+--------------------+
|
||||
|Handle mailboxes on |Admin |Rename or remove |no |
|
||||
|filesystem | |mailboxes on the | |
|
||||
| | |filesystem when they | |
|
||||
|
||||
@@ -190,5 +190,8 @@ following content inside::
|
||||
# Public API communication
|
||||
0 * * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api
|
||||
|
||||
# Generate DKIM keys (they will belong to the user running this job)
|
||||
* * * * * root $PYTHON $INSTANCE/manage.py modo manage_dkim_keys
|
||||
|
||||
|
||||
Now you can continue to the :ref:`webserver` section.
|
||||
|
||||
@@ -13,6 +13,8 @@ from modoboa.lib.form_utils import YesNoField, SeparatorField
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
from modoboa.parameters import forms as param_forms
|
||||
|
||||
from . import constants
|
||||
|
||||
|
||||
class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
app = "admin"
|
||||
@@ -55,6 +57,26 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
)
|
||||
)
|
||||
|
||||
dkim_keys_storage_dir = forms.CharField(
|
||||
label=ugettext_lazy("DKIM keys storage directory"),
|
||||
initial="",
|
||||
help_text=ugettext_lazy(
|
||||
"Absolute path of the directory where DKIM private keys will "
|
||||
"be stored. Make sure this directory belongs to root user "
|
||||
"and is not readable by the outside world."
|
||||
),
|
||||
required=False
|
||||
)
|
||||
|
||||
dkim_default_key_length = forms.ChoiceField(
|
||||
label=ugettext_lazy("Default DKIM key length"),
|
||||
initial=2048,
|
||||
choices=constants.DKIM_KEY_LENGTHS,
|
||||
help_text=ugettext_lazy(
|
||||
"Default length in bits for newly generated DKIM keys."
|
||||
)
|
||||
)
|
||||
|
||||
mbsep = SeparatorField(label=ugettext_lazy("Mailboxes"))
|
||||
|
||||
handle_mailboxes = YesNoField(
|
||||
@@ -173,6 +195,22 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
)
|
||||
return self.cleaned_data["default_mailbox_quota"]
|
||||
|
||||
def clean_dkim_keys_storage_dir(self):
|
||||
"""Check that directory exists."""
|
||||
storage_dir = self.cleaned_data.get("dkim_keys_storage_dir", "")
|
||||
if storage_dir:
|
||||
if not os.path.isdir(storage_dir):
|
||||
raise forms.ValidationError(
|
||||
ugettext_lazy("Directory not found.")
|
||||
)
|
||||
code, output = exec_cmd("which openssl")
|
||||
if code:
|
||||
raise forms.ValidationError(
|
||||
ugettext_lazy(
|
||||
"openssl not found, please make sure it is installed.")
|
||||
)
|
||||
return storage_dir
|
||||
|
||||
def clean(self):
|
||||
"""Check MX options."""
|
||||
cleaned_data = super(AdminParametersForm, self).clean()
|
||||
|
||||
@@ -99,3 +99,9 @@ DOMAIN_TYPES = [
|
||||
("domain", _("Domain")),
|
||||
("relaydomain", _("Relay domain")),
|
||||
]
|
||||
|
||||
DKIM_KEY_LENGTHS = [
|
||||
(1024, "1024"),
|
||||
(2048, "2048"),
|
||||
(4096, "4096"),
|
||||
]
|
||||
|
||||
@@ -43,12 +43,16 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
"press ENTER to add a new input."
|
||||
)
|
||||
)
|
||||
dkim_key_selector = forms.CharField(
|
||||
label=ugettext_lazy("Key selector"), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = (
|
||||
"name", "type", "quota", "default_mailbox_quota", "aliases",
|
||||
"enabled", "enable_dns_checks")
|
||||
"enabled", "enable_dns_checks", "enable_dkim",
|
||||
"dkim_key_selector", "dkim_key_length"
|
||||
)
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.oldname = None
|
||||
@@ -93,6 +97,17 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
|
||||
return name
|
||||
|
||||
def clean_enable_dkim(self):
|
||||
"""Check prerequisites."""
|
||||
enabled = self.cleaned_data.get("enable_dkim")
|
||||
if not enabled:
|
||||
return enabled
|
||||
storage_dir = param_tools.get_global_parameter("dkim_keys_storage_dir")
|
||||
if not storage_dir:
|
||||
raise forms.ValidationError(
|
||||
_("DKIM keys storage directory not configured"))
|
||||
return enabled
|
||||
|
||||
def clean(self):
|
||||
"""Custom fields validation.
|
||||
|
||||
@@ -123,6 +138,10 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
"default_mailbox_quota"]
|
||||
if default_mailbox_quota == 0:
|
||||
self.add_error("default_mailbox_quota", msg)
|
||||
if self.cleaned_data["enable_dkim"]:
|
||||
if not self.cleaned_data.get("dkim_key_selector"):
|
||||
self.add_error(
|
||||
"dkim_key_selector", _("This field is required."))
|
||||
self.aliases = []
|
||||
copied_data = cleaned_data.copy()
|
||||
for k in copied_data.keys():
|
||||
|
||||
@@ -7,6 +7,7 @@ from subcommand.base import SubcommandCommand
|
||||
from .subcommands._mx import CheckMXRecords
|
||||
from .subcommands._export import ExportCommand
|
||||
from .subcommands._import import ImportCommand
|
||||
from .subcommands._manage_dkim_keys import ManageDKIMKeys
|
||||
from .subcommands._repair import Repair
|
||||
|
||||
|
||||
@@ -22,5 +23,6 @@ class Command(SubcommandCommand):
|
||||
"export": ExportCommand,
|
||||
"import": ImportCommand,
|
||||
"check_mx": CheckMXRecords,
|
||||
"manage_dkim_keys": ManageDKIMKeys,
|
||||
"repair": Repair,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Management command to create DKIM keys."""
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from modoboa.lib import sysutils
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from .... import models
|
||||
|
||||
|
||||
class ManageDKIMKeys(BaseCommand):
|
||||
"""Command class."""
|
||||
|
||||
def create_new_dkim_key(self, domain):
|
||||
"""Create a new DKIM key."""
|
||||
storage_dir = param_tools.get_global_parameter("dkim_keys_storage_dir")
|
||||
pkey_path = os.path.join(storage_dir, "{}.pem".format(domain.name))
|
||||
key_size = (
|
||||
domain.dkim_key_length if domain.dkim_key_length
|
||||
else self.default_key_length)
|
||||
code, output = sysutils.exec_cmd(
|
||||
"openssl genrsa -out {} {}".format(pkey_path, key_size))
|
||||
if code:
|
||||
print("Failed to generate DKIM private key for domain {}: {}"
|
||||
.format(domain.name, smart_text(output)))
|
||||
domain.dkim_private_key_path = pkey_path
|
||||
code, output = sysutils.exec_cmd(
|
||||
"openssl rsa -in {} -pubout".format(pkey_path))
|
||||
if code:
|
||||
print("Failed to generate DKIM public key for domain {}: {}"
|
||||
.format(domain.name, smart_text(output)))
|
||||
public_key = ""
|
||||
for cpt, line in enumerate(smart_text(output).splitlines()):
|
||||
if cpt == 0 or line.startswith("-----"):
|
||||
continue
|
||||
public_key += line
|
||||
domain.dkim_public_key = public_key
|
||||
domain.save(update_fields=["dkim_public_key", "dkim_private_key_path"])
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Entry point."""
|
||||
self.default_key_length = param_tools.get_global_parameter(
|
||||
"dkim_default_key_length")
|
||||
qset = models.Domain.objects.filter(
|
||||
enable_dkim=True, dkim_private_key_path="")
|
||||
for domain in qset:
|
||||
self.create_new_dkim_key(domain)
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.8 on 2018-01-12 15:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0011_domain_transport'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='dkim_key_length',
|
||||
field=models.PositiveIntegerField(blank=True, choices=[(1024, '1024'), (2048, '2048'), (4096, '4096')], null=True, verbose_name='Key length'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='dkim_key_selector',
|
||||
field=models.CharField(default='modoboa', max_length=30, verbose_name='Key selector'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='dkim_private_key_path',
|
||||
field=models.CharField(blank=True, max_length=254),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='dkim_public_key',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='enable_dkim',
|
||||
field=models.BooleanField(default=False, help_text='If you activate this feature, a DKIM key will be generated for this domain.', verbose_name='Enable DKIM signing'),
|
||||
),
|
||||
]
|
||||
@@ -58,6 +58,21 @@ class Domain(AdminObject):
|
||||
transport = models.OneToOneField(
|
||||
"transport.Transport", null=True, on_delete=models.SET_NULL)
|
||||
|
||||
enable_dkim = models.BooleanField(
|
||||
ugettext_lazy("Enable DKIM signing"),
|
||||
help_text=ugettext_lazy(
|
||||
"If you activate this feature, a DKIM key will be "
|
||||
"generated for this domain."),
|
||||
default=False
|
||||
)
|
||||
dkim_key_selector = models.CharField(max_length=30, default="modoboa")
|
||||
dkim_key_length = models.PositiveIntegerField(
|
||||
ugettext_lazy("Key length"), choices=constants.DKIM_KEY_LENGTHS,
|
||||
blank=True, null=True
|
||||
)
|
||||
dkim_public_key = models.TextField(blank=True)
|
||||
dkim_private_key_path = models.CharField(max_length=254, blank=True)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_domain", "View domain"),
|
||||
@@ -187,6 +202,12 @@ class Domain(AdminObject):
|
||||
return 0
|
||||
return int(self.used_quota / float(self.quota) * 100)
|
||||
|
||||
@property
|
||||
def bind_format_dkim_public_key(self):
|
||||
"""Return DKIM public key using Bind format."""
|
||||
return "{}._domainkey 10800 IN TXT \"v=DKIM1; k=rsa; p={}\"".format(
|
||||
self.dkim_key_selector, self.dkim_public_key)
|
||||
|
||||
def add_admin(self, account):
|
||||
"""Add a new administrator to this domain.
|
||||
|
||||
|
||||
@@ -223,6 +223,21 @@ Domains.prototype = {
|
||||
"disabled", $target.val() === "False");
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide or show password inputs depending on value.
|
||||
*
|
||||
* @param {bool} value - visibility flag
|
||||
*/
|
||||
toggleDKIMFields: function (value) {
|
||||
if (value) {
|
||||
$('#id_dkim_key_selector').parents(".form-group").show();
|
||||
$('#id_dkim_key_length').parents(".form-group").show();
|
||||
} else {
|
||||
$('#id_dkim_key_selector').parents(".form-group").hide();
|
||||
$('#id_dkim_key_length').parents(".form-group").hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the main form contained in the domain edition modal.
|
||||
*
|
||||
@@ -239,6 +254,11 @@ Domains.prototype = {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$('#id_enable_dkim').click($.proxy(function (e) {
|
||||
var $this = $(e.target);
|
||||
this.toggleDKIMFields($this.prop('checked'));
|
||||
}, this));
|
||||
this.toggleDKIMFields($('#id_enable_dkim').prop('checked'));
|
||||
},
|
||||
|
||||
optionsform_init: function() {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="modal-dialog {% block modal_css_classes %}{% endblock %}">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">{% trans "Close" %}</span></button>
|
||||
<h3 class="modal-title">{% blocktrans trimmed with domain=domain.name %}DKIM public key for {{ domain }}{% endblocktrans %}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info" style="overflow-wrap: break-word;">
|
||||
<strong>{% trans "Raw format" %}</strong><br>
|
||||
{{ domain.dkim_public_key }}
|
||||
</div>
|
||||
<div class="alert alert-info" style="overflow-wrap: break-word;">
|
||||
<strong>{% trans "Bind/named format" %}</strong><br>
|
||||
{{ domain.bind_format_dkim_public_key }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,14 +25,6 @@
|
||||
<h3 class="panel-title">{% trans "Summary" %}</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
{% if enable_mx_checks or enable_dnsbl_checks %}
|
||||
<tr>
|
||||
<th>{% trans "DNS status" %}</th>
|
||||
<td>
|
||||
{% include "admin/_domain_dnschecks_status.html" %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th width="40%">{% trans "Creation date" %}</th>
|
||||
<td>{{ domain.creation }}</td>
|
||||
@@ -94,6 +86,33 @@
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "DNS" %}</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
{% if enable_mx_checks or enable_dnsbl_checks %}
|
||||
<tr>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<td>
|
||||
{% include "admin/_domain_dnschecks_status.html" %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>{% trans "DKIM key" %}</th>
|
||||
<td>
|
||||
{% if domain.dkim_public_key %}
|
||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#domain_dkim_key">
|
||||
{% trans "Show key" %}
|
||||
</button>
|
||||
{% else %}
|
||||
{% trans "Not generated" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">{% trans "Administrators" %}</h3></div>
|
||||
<table id="domadmins" class="table">
|
||||
@@ -101,19 +120,19 @@
|
||||
<tr><th>{% trans "Username" %}</th><th>{% trans "Name" %}</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for da in domain.admins %}
|
||||
<tr>
|
||||
<td>{{ da.username }}</td>
|
||||
<td>{{ da.fullname }}</td>
|
||||
<td name="actions">
|
||||
{% if perms.admin.change_domain %}
|
||||
<a name="removeperm" href="{% url 'admin:permission_remove' %}?domid={{ domain.id }}&daid={{ da.id }}" title="{% trans 'Remove this permission' %}"><span class="fa fa-trash"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3">{% trans "No domain administrator defined yet." %}</td></tr>
|
||||
{% endfor %}
|
||||
{% for da in domain.admins %}
|
||||
<tr>
|
||||
<td>{{ da.username }}</td>
|
||||
<td>{{ da.fullname }}</td>
|
||||
<td name="actions">
|
||||
{% if perms.admin.change_domain %}
|
||||
<a name="removeperm" href="{% url 'admin:permission_remove' %}?domid={{ domain.id }}&daid={{ da.id }}" title="{% trans 'Remove this permission' %}"><span class="fa fa-trash"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3">{% trans "No domain administrator defined yet." %}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -123,4 +142,9 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if domain.dkim_public_key %}
|
||||
<div class="modal fade" id="domain_dkim_key" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
{% include "admin/_domain_dkim_key.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,3 +6,6 @@
|
||||
{% render_fields_group form "aliases" %}
|
||||
{% render_field form.enabled %}
|
||||
{% render_field form.enable_dns_checks %}
|
||||
{% render_field form.enable_dkim %}
|
||||
{% render_field form.dkim_key_selector %}
|
||||
{% render_field form.dkim_key_length %}
|
||||
|
||||
@@ -4,13 +4,21 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import dns.resolver
|
||||
from mock import patch
|
||||
from testfixtures import compare
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
|
||||
from modoboa.core import factories as core_factories
|
||||
from modoboa.core.models import User
|
||||
from modoboa.core.tests.test_views import SETTINGS_SAMPLE
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
|
||||
from . import utils
|
||||
@@ -213,7 +221,8 @@ class DomainTestCase(ModoTestCase):
|
||||
mock_query.side_effect = utils.mock_dns_query_result
|
||||
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
||||
values = {
|
||||
"name": "no-mx.example.com", "quota": 0, "default_mailbox_quota": 0,
|
||||
"name": "no-mx.example.com", "quota": 0,
|
||||
"default_mailbox_quota": 0,
|
||||
"create_dom_admin": True, "dom_admin_username": "toto",
|
||||
"create_aliases": True, "type": "domain", "stepid": "step3",
|
||||
"with_mailbox": True
|
||||
@@ -347,3 +356,66 @@ class DomainTestCase(ModoTestCase):
|
||||
content = response.content.decode('utf-8')
|
||||
self.assertNotIn("Global statistics", content)
|
||||
self.assertIn("Per-domain statistics", content)
|
||||
|
||||
|
||||
class DKIMTestCase(ModoTestCase):
|
||||
"""Test case for DKIM."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls): # noqa: N802
|
||||
"""Create test data."""
|
||||
super(DKIMTestCase, cls).setUpTestData()
|
||||
factories.populate_database()
|
||||
|
||||
def setUp(self):
|
||||
super(DKIMTestCase, self).setUp()
|
||||
self.workdir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.workdir)
|
||||
|
||||
def test_global_settings(self):
|
||||
"""Check validation rules."""
|
||||
url = reverse("core:parameters")
|
||||
settings = SETTINGS_SAMPLE.copy()
|
||||
settings["admin-dkim_keys_storage_dir"] = "/wrong"
|
||||
response = self.client.post(url, settings, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
compare(response.json(), {
|
||||
"form_errors": {"dkim_keys_storage_dir": ["Directory not found."]},
|
||||
"prefix": "admin"
|
||||
})
|
||||
settings["admin-dkim_keys_storage_dir"] = self.workdir
|
||||
response = self.client.post(url, settings, format="json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_dkim_key_creation(self):
|
||||
"""Test DKIM key creation."""
|
||||
values = {
|
||||
"name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100,
|
||||
"create_dom_admin": False, "type": "domain", "stepid": "step3",
|
||||
"enable_dkim": True, "dkim_key_selector": ""
|
||||
}
|
||||
response = self.ajax_post(reverse("admin:domain_add"), values, 400)
|
||||
self.assertEqual(
|
||||
response["form_errors"]["enable_dkim"][0],
|
||||
"DKIM keys storage directory not configured")
|
||||
|
||||
self.set_global_parameter("dkim_keys_storage_dir", self.workdir)
|
||||
response = self.ajax_post(reverse("admin:domain_add"), values, 400)
|
||||
self.assertEqual(
|
||||
response["form_errors"]["dkim_key_selector"][0],
|
||||
"This field is required.")
|
||||
|
||||
values["dkim_key_selector"] = "default"
|
||||
self.ajax_post(reverse("admin:domain_add"), values)
|
||||
|
||||
call_command("modo", "manage_dkim_keys")
|
||||
key_path = os.path.join(self.workdir, "{}.pem".format(values["name"]))
|
||||
self.assertTrue(os.path.exists(key_path))
|
||||
|
||||
domain = Domain.objects.get(name=values["name"])
|
||||
url = reverse("admin:domain_detail", args=[domain.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertContains(
|
||||
response, escape(domain.bind_format_dkim_public_key))
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.8 on 2018-01-12 15:42
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_auto_20171010_1746'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='language',
|
||||
field=models.CharField(choices=[('cs', '\u010de\u0161tina'), ('de', 'deutsch'), ('en', 'english'), ('el_GR', '\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'), ('es', 'espa\xf1ol'), ('fr', 'fran\xe7ais'), ('it', 'italiano'), ('ja_JP', '\u65e5\u672c\u306e'), ('nl', 'nederlands'), ('pt_PT', 'portugu\xeas'), ('pt_BR', 'portugu\xeas (BR)'), ('pl_PL', 'polski'), ('ru', '\u0440\u0443\u0441\u0441\u043a\u0438\u0439'), ('sv', 'svenska')], default='en', help_text='Prefered language to display pages.', max_length=10, verbose_name='language'),
|
||||
),
|
||||
]
|
||||
@@ -15,6 +15,63 @@ from .. import factories
|
||||
from .. import models
|
||||
from .. import signals
|
||||
|
||||
SETTINGS_SAMPLE = {
|
||||
"core-check_new_versions": "True",
|
||||
"core-log_maximum_age": "365",
|
||||
"core-ldap_auth_method": "searchbind",
|
||||
"limits-deflt_domain_mailbox_aliases_limit": "0",
|
||||
"core-send_statistics": "True",
|
||||
"core-default_top_redirection": "user",
|
||||
"core-sender_address": "noreply@modoboa.org",
|
||||
"admin-auto_account_removal": "False",
|
||||
"core-ldap_server_port": "389",
|
||||
"core-rounds_number": "70000",
|
||||
"core-ldap_bind_password": "",
|
||||
"limits-deflt_domain_domain_aliases_limit": "0",
|
||||
"core-secret_key": ":?j3]QPWo!.'_c4n",
|
||||
"limits-deflt_domain_domain_admins_limit": "0",
|
||||
"limits-enable_admin_limits": "True",
|
||||
"core-ldap_bind_dn": "",
|
||||
"core-enable_api_communication": "True",
|
||||
"limits-deflt_user_domain_admins_limit": "0",
|
||||
"limits-enable_domain_limits": "False",
|
||||
"csrfmiddlewaretoken": "SGgMVZsA4TPqoiV786TMST6xgOlhAf4F",
|
||||
"limits-deflt_user_mailboxes_limit": "0",
|
||||
"core-password_scheme": "sha512crypt",
|
||||
"core-items_per_page": "30",
|
||||
"limits-deflt_user_mailbox_aliases_limit": "0",
|
||||
"limits-deflt_domain_mailboxes_limit": "0",
|
||||
"core-ldap_is_active_directory": "False",
|
||||
"core-ldap_group_type": "posixgroup",
|
||||
"limits-deflt_user_domain_aliases_limit": "0",
|
||||
"core-top_notifications_check_interval": "30",
|
||||
"core-ldap_password_attribute": "userPassword",
|
||||
"admin-auto_create_domain_and_mailbox": "True",
|
||||
"admin-enable_dnsbl_checks": "True",
|
||||
"admin-default_domain_quota": "0",
|
||||
"admin-default_mailbox_quota": "0",
|
||||
"core-ldap_server_address": "localhost",
|
||||
"core-authentication_type": "local",
|
||||
"core-ldap_admin_groups": "",
|
||||
"core-ldap_groups_search_base": "",
|
||||
"admin-enable_mx_checks": "True",
|
||||
"core-ldap_search_base": "",
|
||||
"admin-valid_mxs": "",
|
||||
"limits-deflt_user_domains_limit": "0",
|
||||
"core-ldap_search_filter": "(mail=%(user)s)",
|
||||
"core-ldap_secured": "none",
|
||||
"core-ldap_user_dn_template": "",
|
||||
"core-default_password": "Toto1000",
|
||||
"limits-deflt_user_quota_limit": "0",
|
||||
"core-hide_features_widget": "False",
|
||||
"core-inactive_account_threshold": "30",
|
||||
"admin-domains_must_have_authorized_mx": False,
|
||||
"core-random_password_length": "8",
|
||||
"admin-create_alias_on_mbox_rename": False,
|
||||
"admin-dkim_keys_storage_dir": "",
|
||||
"admin-dkim_default_key_length": 1024
|
||||
}
|
||||
|
||||
|
||||
def announcement(sender, location, **kwargs):
|
||||
"""Simpler handler."""
|
||||
@@ -106,61 +163,6 @@ class DashboardTestCase(ModoTestCase):
|
||||
class SettingsTestCase(ModoTestCase):
|
||||
"""Settings tests."""
|
||||
|
||||
settings_sample = {
|
||||
"core-check_new_versions": "True",
|
||||
"core-log_maximum_age": "365",
|
||||
"core-ldap_auth_method": "searchbind",
|
||||
"limits-deflt_domain_mailbox_aliases_limit": "0",
|
||||
"core-send_statistics": "True",
|
||||
"core-default_top_redirection": "user",
|
||||
"core-sender_address": "noreply@modoboa.org",
|
||||
"admin-auto_account_removal": "False",
|
||||
"core-ldap_server_port": "389",
|
||||
"core-rounds_number": "70000",
|
||||
"core-ldap_bind_password": "",
|
||||
"limits-deflt_domain_domain_aliases_limit": "0",
|
||||
"core-secret_key": ":?j3]QPWo!.'_c4n",
|
||||
"limits-deflt_domain_domain_admins_limit": "0",
|
||||
"limits-enable_admin_limits": "True",
|
||||
"core-ldap_bind_dn": "",
|
||||
"core-enable_api_communication": "True",
|
||||
"limits-deflt_user_domain_admins_limit": "0",
|
||||
"limits-enable_domain_limits": "False",
|
||||
"csrfmiddlewaretoken": "SGgMVZsA4TPqoiV786TMST6xgOlhAf4F",
|
||||
"limits-deflt_user_mailboxes_limit": "0",
|
||||
"core-password_scheme": "sha512crypt",
|
||||
"core-items_per_page": "30",
|
||||
"limits-deflt_user_mailbox_aliases_limit": "0",
|
||||
"limits-deflt_domain_mailboxes_limit": "0",
|
||||
"core-ldap_is_active_directory": "False",
|
||||
"core-ldap_group_type": "posixgroup",
|
||||
"limits-deflt_user_domain_aliases_limit": "0",
|
||||
"core-top_notifications_check_interval": "30",
|
||||
"core-ldap_password_attribute": "userPassword",
|
||||
"admin-auto_create_domain_and_mailbox": "True",
|
||||
"admin-enable_dnsbl_checks": "True",
|
||||
"admin-default_domain_quota": "0",
|
||||
"admin-default_mailbox_quota": "0",
|
||||
"core-ldap_server_address": "localhost",
|
||||
"core-authentication_type": "local",
|
||||
"core-ldap_admin_groups": "",
|
||||
"core-ldap_groups_search_base": "",
|
||||
"admin-enable_mx_checks": "True",
|
||||
"core-ldap_search_base": "",
|
||||
"admin-valid_mxs": "",
|
||||
"limits-deflt_user_domains_limit": "0",
|
||||
"core-ldap_search_filter": "(mail=%(user)s)",
|
||||
"core-ldap_secured": "none",
|
||||
"core-ldap_user_dn_template": "",
|
||||
"core-default_password": "Toto1000",
|
||||
"limits-deflt_user_quota_limit": "0",
|
||||
"core-hide_features_widget": "False",
|
||||
"core-inactive_account_threshold": "30",
|
||||
"admin-domains_must_have_authorized_mx": False,
|
||||
"core-random_password_length": "8",
|
||||
"admin-create_alias_on_mbox_rename": False,
|
||||
}
|
||||
|
||||
def test_get_settings(self):
|
||||
"""Test settings display."""
|
||||
url = reverse("core:parameters")
|
||||
@@ -171,10 +173,11 @@ class SettingsTestCase(ModoTestCase):
|
||||
def test_save_settings(self):
|
||||
"""Test settings save."""
|
||||
url = reverse("core:parameters")
|
||||
response = self.client.post(url, self.settings_sample, format="json")
|
||||
settings = SETTINGS_SAMPLE.copy()
|
||||
response = self.client.post(url, settings, format="json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.settings_sample["core-rounds_number"] = ""
|
||||
response = self.client.post(url, self.settings_sample, format="json")
|
||||
settings["core-rounds_number"] = ""
|
||||
response = self.client.post(url, settings, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
compare(response.json(), {
|
||||
"form_errors": {"rounds_number": ["This field is required."]},
|
||||
|
||||
@@ -298,4 +298,4 @@ table td[name="actions"] {
|
||||
/* Dashboard */
|
||||
.slogan {
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ TwocolsNav.prototype = {
|
||||
success_cb: $.proxy(this.save_cb, this)
|
||||
});
|
||||
}, this));
|
||||
$(document).on('submit', '#parameters_form', function(evt) {
|
||||
$(document).on('submit', '#parameters_form', $.proxy(function(evt) {
|
||||
simple_ajax_form_post(evt, {
|
||||
formid: $(this).attr('id'),
|
||||
modal: false,
|
||||
@@ -46,7 +46,7 @@ TwocolsNav.prototype = {
|
||||
$('a[data-app="' + data.prefix + '"]').tab('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
}, this));
|
||||
},
|
||||
|
||||
update_content: function(data) {
|
||||
|
||||
Reference in New Issue
Block a user