mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-19 05:58:34 +02:00
Merge branch 'master' into refacto/vuejs_interface
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
---
|
||||
name: 'Bug Report'
|
||||
about: 'Report a reproducible bug in the software.'
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
# Impacted versions
|
||||
|
||||
* OS Type: Debian/Ubuntu, Centos/Redhat, Arch, Gentoo, Slack, ...
|
||||
* OS Version: Number or Name
|
||||
* Database Type: PostgreSQL/MySQL
|
||||
* Database Type: PostgreSQL / MySQL
|
||||
* Database version: X.y
|
||||
* Modoboa: X.X.x
|
||||
* installer used: Yes/No ?
|
||||
@@ -12,6 +20,13 @@
|
||||
|
||||
# Current behavior
|
||||
|
||||
<!--
|
||||
|
||||
Explain the behavior you're seeing that you think is a bug, and explain how you
|
||||
think things should behave instead.
|
||||
|
||||
-->
|
||||
|
||||
# Expected behavior
|
||||
|
||||
# Video/Screenshot link (optional)
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Modoboa Support
|
||||
url: https://github.com/modoboa/modoboa/discussions
|
||||
about: Please ask and answer questions here.
|
||||
- name: Sponsoring
|
||||
ur: https://modoboa.org/en/sponsoring/
|
||||
about: You love Modoboa and want to sponsor it
|
||||
@@ -66,7 +66,9 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install postfix librrd-dev rrdtool libldap2-dev libsasl2-dev libssl-dev
|
||||
sudo apt-get update -y \
|
||||
&& sudo apt-get install -y \
|
||||
postfix librrd-dev rrdtool libldap2-dev libsasl2-dev libssl-dev
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r ldap-requirements.txt
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN python3 -m venv $VIRTUAL_ENV
|
||||
WORKDIR /tmp
|
||||
COPY requirements.txt /tmp
|
||||
COPY test-requirements.txt /tmp
|
||||
RUN python -m pip install -U pip
|
||||
RUN pip install -r requirements.txt -r test-requirements.txt
|
||||
|
||||
FROM base as run
|
||||
|
||||
+13
-2
@@ -31,7 +31,7 @@ The *admin* application exposes several parameters, they are presented below:
|
||||
|Authentication type |General |The backend used for |Local |
|
||||
| | |authentication | |
|
||||
+--------------------+---------------------+-------------------------------+--------------------+
|
||||
|Default password |General |Scheme used to crypt |crypt |
|
||||
|Default password |General |Scheme used to crypt |sha512crypt |
|
||||
|scheme | |mailbox passwords | |
|
||||
+--------------------+---------------------+-------------------------------+--------------------+
|
||||
|Rounds |General |Number of rounds (only used by |70000 |
|
||||
@@ -144,6 +144,13 @@ The *admin* application exposes several parameters, they are presented below:
|
||||
| | | | |
|
||||
+--------------------+---------------------+-------------------------------+--------------------+
|
||||
|
||||
.. warning::
|
||||
|
||||
If Dovecot is not running on the same host than Modoboa, you will have
|
||||
to define which password schemes are supported. To do so, open the
|
||||
:file:`settings.py` file and add a ``DOVECOT_SUPPORTED_SCHEMES``
|
||||
variable with the output of the command: ``doveadm pw -l``.
|
||||
|
||||
.. note::
|
||||
|
||||
If you are not familiar with virtual domain hosting, you should
|
||||
@@ -202,7 +209,11 @@ Host configuration
|
||||
.. note::
|
||||
|
||||
This section is only relevant when Modoboa handles mailboxes
|
||||
renaming and removal from the filesystem.
|
||||
renaming and removal from the filesystem, which requires that
|
||||
Dovecot is installed and running on this host. If it is installed
|
||||
at a non-standard directory, paths to its binaries can be set in the
|
||||
:file:`settings.py` file with the ``DOVECOT_LOOKUP_PATH`` and
|
||||
``DOVEADM_LOOKUP_PATH`` variables.
|
||||
|
||||
To manipulate mailboxes on the filesystem, you must allow the user who
|
||||
runs Modoboa to execute commands as the user who owns mailboxes.
|
||||
|
||||
@@ -24,6 +24,12 @@ setup, go to the next section.
|
||||
|
||||
.. _venv_for_dev:
|
||||
|
||||
Prerequisites For Development
|
||||
=============================
|
||||
If you encounter errors about missing tools, please ensure you have them installed with the foollowing:
|
||||
|
||||
$ sudo apt install librrd-dev
|
||||
|
||||
Prepare a virtual environment
|
||||
=============================
|
||||
|
||||
|
||||
@@ -55,9 +55,14 @@ following system packages according to your distribution:
|
||||
.. note::
|
||||
|
||||
Alternatively, you could rely on your distribution packages for the Modoboa
|
||||
dependencies which require compilation - e.g. ``psycopg2`` - if the version
|
||||
dependencies which require compilation - e.g. ``rrdtool`` - if the version
|
||||
is compatible. In this case, you have to create your virtual environment
|
||||
with the ``--system-site-packages`` option.
|
||||
with the ``--system-site-packages`` option, and the required system
|
||||
packages will be:
|
||||
|
||||
+---------------------------------------+
|
||||
| python3-wheel python3-rrdtool rrdtool |
|
||||
+---------------------------------------+
|
||||
|
||||
Then, install Modoboa by running:
|
||||
|
||||
@@ -184,6 +189,9 @@ you just have to run the following command:
|
||||
``<mysql|postgres>://[user:pass@][host:port]/dbname`` **OR**
|
||||
``sqlite:////full/path/to/your/database/file.sqlite``.
|
||||
|
||||
Fox example, if you were using postgres, you could setup your command like this:
|
||||
``modoboa-admin.py deploy instance_name --collectstatic --domain example.com --dburl default:postgres://user:pass@[localhost]/modoboa``
|
||||
|
||||
The command will ask you a few questions, answer them and you're
|
||||
done.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from . import lib, models, serializers
|
||||
summary="Create a new domain"
|
||||
)
|
||||
)
|
||||
class DomainViewSet(viewsets.ModelViewSet):
|
||||
class DomainViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
|
||||
"""Domain viewset."""
|
||||
|
||||
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
|
||||
@@ -63,7 +63,8 @@ class DomainAliasFilterSet(dj_filters.FilterSet):
|
||||
fields = ["domain"]
|
||||
|
||||
|
||||
class DomainAliasViewSet(lib_viewsets.ExpandableModelViewSet):
|
||||
class DomainAliasViewSet(lib_viewsets.RevisionModelMixin,
|
||||
lib_viewsets.ExpandableModelViewSet):
|
||||
"""ViewSet for DomainAlias."""
|
||||
|
||||
filter_backends = (dj_filters.DjangoFilterBackend, )
|
||||
@@ -83,7 +84,7 @@ class DomainAliasViewSet(lib_viewsets.ExpandableModelViewSet):
|
||||
return context
|
||||
|
||||
|
||||
class AccountViewSet(viewsets.ModelViewSet):
|
||||
class AccountViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
|
||||
"""ViewSet for User/Mailbox."""
|
||||
|
||||
filter_backends = [filters.SearchFilter]
|
||||
@@ -178,7 +179,7 @@ class AccountViewSet(viewsets.ModelViewSet):
|
||||
return Response(body)
|
||||
|
||||
|
||||
class AliasViewSet(viewsets.ModelViewSet):
|
||||
class AliasViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
|
||||
"""
|
||||
create:
|
||||
Create a new alias instance.
|
||||
@@ -203,7 +204,8 @@ class AliasViewSet(viewsets.ModelViewSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class SenderAddressViewSet(viewsets.ModelViewSet):
|
||||
class SenderAddressViewSet(lib_viewsets.RevisionModelMixin,
|
||||
viewsets.ModelViewSet):
|
||||
"""View set for SenderAddress model."""
|
||||
|
||||
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
|
||||
|
||||
@@ -147,14 +147,6 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
)
|
||||
)
|
||||
|
||||
mailboxes_owner = forms.CharField(
|
||||
label=ugettext_lazy("Mailboxes owner"),
|
||||
initial="vmail",
|
||||
help_text=ugettext_lazy(
|
||||
"The UNIX account who owns mailboxes on the filesystem"
|
||||
)
|
||||
)
|
||||
|
||||
default_mailbox_quota = forms.IntegerField(
|
||||
label=ugettext_lazy("Default mailbox quota"),
|
||||
initial=0,
|
||||
@@ -200,8 +192,7 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
# Visibility rules
|
||||
visibility_rules = {
|
||||
"valid_mxs": "enable_mx_checks=True",
|
||||
"domains_must_have_authorized_mx": "enable_mx_checks=True",
|
||||
"mailboxes_owner": "handle_mailboxes=True",
|
||||
"domains_must_have_authorized_mx": "enable_mx_checks=True"
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -236,7 +227,6 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
hide_fields = True
|
||||
if hide_fields:
|
||||
del self.fields["handle_mailboxes"]
|
||||
del self.fields["mailboxes_owner"]
|
||||
|
||||
def clean_default_domain_quota(self):
|
||||
"""Ensure quota is a positive integer."""
|
||||
|
||||
@@ -206,9 +206,8 @@ class AccountFormGeneral(forms.ModelForm):
|
||||
account.role = self.cleaned_data["role"]
|
||||
if hasattr(account, "mailbox"):
|
||||
# Update forward status according to account status
|
||||
models.Alias.objects.filter(
|
||||
address=account.email, internal=False).update(
|
||||
enabled=account.is_active)
|
||||
models.Alias.objects.filter(address=account.email).update(
|
||||
enabled=account.is_active)
|
||||
return account
|
||||
|
||||
|
||||
|
||||
@@ -198,6 +198,8 @@ def get_dns_records(name, typ, resolver=None):
|
||||
logger.warning(
|
||||
_("DNS resolution timeout, unable to query %s at the moment") %
|
||||
name, exc_info=e)
|
||||
except dns.name.NameTooLong as e:
|
||||
logger.error(_("DNS name is too long: %s" % name), exc_info=e)
|
||||
else:
|
||||
return dns_answers
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.12 on 2020-12-04 08:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0017_alarm'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='alarm',
|
||||
name='title',
|
||||
field=models.TextField(),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 2.2.17 on 2021-01-28 13:40
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_disabled_accounts_aliases(apps, schema_editor):
|
||||
User = apps.get_model('core', 'User')
|
||||
Alias = apps.get_model('admin', 'Alias')
|
||||
|
||||
disabled_accounts = User.objects.filter(
|
||||
mailbox__isnull=False, is_active=False)
|
||||
|
||||
Alias.objects.filter(
|
||||
address__in=list(disabled_accounts.values_list('email', flat=True)),
|
||||
enabled=True,
|
||||
).update(enabled=False)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0018_auto_20201204_0935'),
|
||||
('core', '0022_user_tfa_enabled'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_disabled_accounts_aliases),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.17 on 2021-01-30 22:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0019_update_disabled_accounts_aliases'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
name='name',
|
||||
field=models.CharField(help_text='The domain name', max_length=253, unique=True, verbose_name='name'),
|
||||
),
|
||||
]
|
||||
@@ -33,7 +33,7 @@ class Alarm(models.Model):
|
||||
db_index=True
|
||||
)
|
||||
|
||||
title = models.CharField(max_length=150)
|
||||
title = models.TextField()
|
||||
internal_name = models.CharField(max_length=120)
|
||||
|
||||
objects = models.Manager.from_queryset(AlarmQuerySet)()
|
||||
|
||||
@@ -23,7 +23,7 @@ from . import mixins
|
||||
class Domain(mixins.MessageLimitMixin, AdminObject):
|
||||
"""Mail domain."""
|
||||
|
||||
name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True,
|
||||
name = models.CharField(ugettext_lazy("name"), max_length=253, unique=True,
|
||||
help_text=ugettext_lazy("The domain name"))
|
||||
quota = models.PositiveIntegerField(
|
||||
default=0,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""Models related to mailboxes management."""
|
||||
|
||||
import os
|
||||
import pwd
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -153,14 +150,7 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject):
|
||||
if not admin_params.get("handle_mailboxes"):
|
||||
return None
|
||||
if self.__mail_home is None:
|
||||
curuser = pwd.getpwuid(os.getuid()).pw_name
|
||||
mbowner = admin_params["mailboxes_owner"]
|
||||
options = {}
|
||||
if curuser != mbowner:
|
||||
options["sudo_user"] = mbowner
|
||||
code, output = doveadm_cmd(
|
||||
"user -f home %s" % self.full_address, **options
|
||||
)
|
||||
code, output = doveadm_cmd("user -f home %s" % self.full_address)
|
||||
if code:
|
||||
raise lib_exceptions.InternalError(
|
||||
_("Failed to retrieve mailbox location (%s)") % output)
|
||||
|
||||
@@ -119,8 +119,10 @@ class AccountTestCase(ModoTestCase):
|
||||
account = User.objects.get(username=values["username"])
|
||||
self.assertEqual(account.language, "fr")
|
||||
|
||||
def test_forward_updated_on_edit(self):
|
||||
def test_aliases_update_on_disable(self):
|
||||
"""Check if aliases are updated when account is disabled."""
|
||||
account = User.objects.get(username="user@test.com")
|
||||
internal = models.Alias.objects.get(address=account.email, internal=True)
|
||||
forward = models.Alias.objects.create(address=account.email, internal=False)
|
||||
values = {
|
||||
"username": "user@test.com",
|
||||
@@ -133,12 +135,16 @@ class AccountTestCase(ModoTestCase):
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[account.id]), values
|
||||
)
|
||||
internal.refresh_from_db()
|
||||
self.assertFalse(internal.enabled)
|
||||
forward.refresh_from_db()
|
||||
self.assertFalse(forward.enabled)
|
||||
values["is_active"] = True
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[account.id]), values
|
||||
)
|
||||
internal.refresh_from_db()
|
||||
self.assertTrue(internal.enabled)
|
||||
forward.refresh_from_db()
|
||||
self.assertTrue(forward.enabled)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework.authtoken.models import Token
|
||||
from reversion.models import Version
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
from modoboa.core import factories as core_factories, models as core_models
|
||||
@@ -51,8 +52,9 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
url, {"name": "test3.com", "quota": 0, "default_mailbox_quota": 10}
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(
|
||||
models.Domain.objects.filter(name="test3.com").exists())
|
||||
domain = models.Domain.objects.get(name="test3.com")
|
||||
self.assertEqual(Version.objects.get_for_object(domain).count(), 1)
|
||||
|
||||
response = self.client.post(url, {})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("name", response.data)
|
||||
|
||||
@@ -51,9 +51,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
|
||||
|
||||
password_scheme = forms.ChoiceField(
|
||||
label=ugettext_lazy("Default password scheme"),
|
||||
choices=[(hasher.name, ugettext_lazy(hasher.label))
|
||||
for hasher in PasswordHasher.get_password_hashers()
|
||||
if hasher().scheme in get_dovecot_schemes()],
|
||||
choices=[],
|
||||
initial="sha512crypt",
|
||||
help_text=ugettext_lazy("Scheme used to crypt mailbox passwords"),
|
||||
)
|
||||
@@ -530,6 +528,11 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
|
||||
self._add_visibilty_rules(
|
||||
sms_backends.get_all_backend_visibility_rules()
|
||||
)
|
||||
self.fields["password_scheme"].choices = [
|
||||
(hasher.name, ugettext_lazy(hasher.label))
|
||||
for hasher in PasswordHasher.get_password_hashers()
|
||||
if hasher().scheme in get_dovecot_schemes()
|
||||
]
|
||||
|
||||
def _add_dynamic_fields(self):
|
||||
new_fields = OrderedDict()
|
||||
|
||||
@@ -114,15 +114,15 @@ MIDDLEWARE = (
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
# 'modoboa.lib.authbackends.LDAPBackend',
|
||||
# 'modoboa.lib.authbackends.SMTPBackend',
|
||||
#'modoboa.lib.authbackends.LDAPBackend',
|
||||
#'modoboa.lib.authbackends.SMTPBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
# SMTP authentication
|
||||
# AUTH_SMTP_SERVER_ADDRESS = 'localhost'
|
||||
# AUTH_SMTP_SERVER_PORT = 25
|
||||
# AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted
|
||||
#AUTH_SMTP_SERVER_ADDRESS = 'localhost'
|
||||
#AUTH_SMTP_SERVER_PORT = 25
|
||||
#AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted
|
||||
|
||||
|
||||
TEMPLATES = [
|
||||
@@ -205,9 +205,17 @@ SPECTACULAR_SETTINGS = {
|
||||
}
|
||||
|
||||
# Modoboa settings
|
||||
# MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png")
|
||||
|
||||
# DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', )
|
||||
#MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png")
|
||||
|
||||
# Path to Dovecot binaries in case of a non-standard installation
|
||||
#DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', )
|
||||
#DOVEADM_LOOKUP_PATH = ('/path/to/doveadm', )
|
||||
|
||||
# List of supported schemes if doveadm is not available, given by: doveadm pw -l
|
||||
#DOVECOT_SUPPORTED_SCHEMES = 'SHA512-CRYPT SHA256-CRYPT'
|
||||
|
||||
# DOVECOT_USER = 'vmail'
|
||||
|
||||
MODOBOA_API_URL = 'https://api.modoboa.org/1/'
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Password hashers for Modoboa.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from modoboa.core.password_hashers.advanced import ( # NOQA:F401
|
||||
@@ -32,17 +33,25 @@ def get_password_hasher(scheme):
|
||||
|
||||
|
||||
def get_dovecot_schemes():
|
||||
"""Return schemes supported by dovecot"""
|
||||
supported_schemes = None
|
||||
try:
|
||||
_, schemes = doveadm_cmd("pw -l")
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
supported_schemes = ["{{{}}}".format(smart_text(scheme))
|
||||
for scheme in schemes.split()]
|
||||
"""Return schemes supported by Dovecot.
|
||||
|
||||
if not supported_schemes:
|
||||
supported_schemes = ['{PLAIN}']
|
||||
It first try to get supported schemes fron the settings, then from the
|
||||
doveadm output, and fallback to {MD5-CRYPT} and {PLAIN} if the command
|
||||
is not found.
|
||||
|
||||
return supported_schemes
|
||||
:return: A list of supported '{SCHEME}'
|
||||
"""
|
||||
schemes = getattr(settings, "DOVECOT_SUPPORTED_SCHEMES", None)
|
||||
default_schemes = "MD5-CRYPT PLAIN"
|
||||
|
||||
if not schemes:
|
||||
try:
|
||||
retcode, schemes = doveadm_cmd("pw -l")
|
||||
except OSError:
|
||||
schemes = default_schemes
|
||||
else:
|
||||
if retcode:
|
||||
schemes = default_schemes
|
||||
|
||||
return ["{{{}}}".format(smart_text(scheme))
|
||||
for scheme in schemes.split()]
|
||||
|
||||
@@ -19,9 +19,9 @@ from modoboa.core.password_hashers import (
|
||||
from modoboa.lib.tests import NO_SMTP, ModoTestCase
|
||||
from .. import factories, models
|
||||
|
||||
DOVEADM_TEST_PATH = "{}/doveadm".format(os.path.dirname(__file__))
|
||||
|
||||
|
||||
@override_settings(
|
||||
DOVEADM_LOOKUP_PATH=["{}/doveadm".format(os.path.dirname(__file__))])
|
||||
class AuthenticationTestCase(ModoTestCase):
|
||||
"""Validate authentication scenarios."""
|
||||
|
||||
@@ -89,6 +89,7 @@ class AuthenticationTestCase(ModoTestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith(reverse("core:dashboard")))
|
||||
|
||||
@override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH])
|
||||
def test_password_schemes(self):
|
||||
"""Validate password scheme changes."""
|
||||
username = "user@test.com"
|
||||
@@ -130,6 +131,7 @@ class AuthenticationTestCase(ModoTestCase):
|
||||
self.assertTrue(user.password.startswith(pw_hash.scheme))
|
||||
|
||||
@skipUnless(argon2, "argon2-cffi not installed")
|
||||
@override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH])
|
||||
def test_password_argon2_parameter_change(self):
|
||||
"""Validate hash parameter update on login works with argon2."""
|
||||
username = "user@test.com"
|
||||
@@ -169,8 +171,16 @@ class AuthenticationTestCase(ModoTestCase):
|
||||
self.assertEqual(parameters.memory_cost, 1000)
|
||||
self.assertEqual(parameters.parallelism, 2)
|
||||
|
||||
def test_supported_schemes(self):
|
||||
"""Validate dovecot supported schemes."""
|
||||
@mock.patch("modoboa.lib.sysutils.exec_cmd")
|
||||
def test_dovecot_supported_schemes(self, exec_cmd_mock):
|
||||
"""Validate Dovecot supported schemes with fake command output."""
|
||||
exec_cmd_mock.return_value = (
|
||||
0,
|
||||
"MD5 MD5-CRYPT SHA SHA1 SHA256 SHA512 SMD5 SSHA SSHA256 SSHA512 "
|
||||
"PLAIN CLEAR CLEARTEXT PLAIN-TRUNC CRAM-MD5 SCRAM-SHA-1 HMAC-MD5 "
|
||||
"DIGEST-MD5 PLAIN-MD4 PLAIN-MD5 LDAP-MD5 LANMAN NTLM OTP SKEY RPA "
|
||||
"PBKDF2 CRYPT SHA256-CRYPT SHA512-CRYPT"
|
||||
)
|
||||
supported_schemes = get_dovecot_schemes()
|
||||
self.assertEqual(supported_schemes,
|
||||
["{MD5}",
|
||||
@@ -204,6 +214,17 @@ class AuthenticationTestCase(ModoTestCase):
|
||||
"{SHA256-CRYPT}",
|
||||
"{SHA512-CRYPT}"])
|
||||
|
||||
@override_settings(DOVECOT_SUPPORTED_SCHEMES="SHA1 SHA512-CRYPT")
|
||||
def test_dovecot_supported_schemes_from_settings(self):
|
||||
"""Validate dovecot supported schemes from the settings."""
|
||||
supported_schemes = get_dovecot_schemes()
|
||||
self.assertEqual(supported_schemes, ["{SHA1}", "{SHA512-CRYPT}"])
|
||||
|
||||
def test_dovecot_default_schemes(self):
|
||||
"""Check default scheme if doveadm is not found."""
|
||||
supported_schemes = get_dovecot_schemes()
|
||||
self.assertEqual(supported_schemes, ["{MD5-CRYPT}", "{PLAIN}"])
|
||||
|
||||
|
||||
class PasswordResetTestCase(ModoTestCase):
|
||||
"""Test password reset service."""
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# 李 正 <364109980@qq.com>, 2021
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-05-06 13:15+0200\n"
|
||||
"PO-Revision-Date: 2020-05-11 18:45+0000\n"
|
||||
"Last-Translator: 李 正 <364109980@qq.com>, 2021\n"
|
||||
"Language-Team: Chinese (China) (https://www.transifex.com/tonio/teams/13749/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: zh_CN\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: ldapsync/lib.py:84
|
||||
msgid "Failed to create LDAP account: {}"
|
||||
msgstr "无法创建LDAP帐户: {}"
|
||||
|
||||
#: ldapsync/lib.py:124
|
||||
msgid "Failed to update LDAP account: {}"
|
||||
msgstr "无法更新LDAP帐户: {}"
|
||||
|
||||
#: ldapsync/lib.py:138
|
||||
msgid "Failed to delete LDAP account: {}"
|
||||
msgstr "无法删除LDAP帐户: {}"
|
||||
|
||||
#: ldapsync/lib.py:149
|
||||
msgid "Failed to disable LDAP account: {}"
|
||||
msgstr "无法关闭LDAP帐户: {}"
|
||||
@@ -5,6 +5,7 @@ This module extra functions/shortcuts to communicate with the system
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
@@ -42,8 +43,7 @@ def exec_cmd(cmd, sudo_user=None, pinput=None, capture_output=True, **kwargs):
|
||||
return process.returncode, output
|
||||
|
||||
|
||||
def doveadm_cmd(params, sudo_user=None, pinput=None,
|
||||
capture_output=True, **kwargs):
|
||||
def doveadm_cmd(params, pinput=None, capture_output=True, **kwargs):
|
||||
"""Execute doveadm command.
|
||||
|
||||
Run doveadm command using the current user. Set :keyword:`sudo_user` if
|
||||
@@ -69,14 +69,17 @@ def doveadm_cmd(params, sudo_user=None, pinput=None,
|
||||
if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
|
||||
dpath = fpath
|
||||
break
|
||||
dovecot_user = getattr(settings, "DOVECOT_USER", "vmail")
|
||||
curuser = pwd.getpwuid(os.getuid()).pw_name
|
||||
sudo_user = dovecot_user if curuser != dovecot_user else None
|
||||
if dpath:
|
||||
return exec_cmd("{} {}".format(dpath, params),
|
||||
sudo_user=sudo_user,
|
||||
pinput=pinput,
|
||||
capture_output=capture_output,
|
||||
**kwargs)
|
||||
else:
|
||||
raise OSError("doveadm command not found")
|
||||
|
||||
raise OSError("doveadm command not found")
|
||||
|
||||
|
||||
def guess_extension_name():
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""ViewSet related mixins and tools."""
|
||||
|
||||
from rest_framework import viewsets
|
||||
from reversion.views import RevisionMixin
|
||||
|
||||
|
||||
class ExpandableSerializerMixin:
|
||||
@@ -32,3 +33,11 @@ class ExpandableModelViewSet(ExpandableSerializerMixin,
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RevisionModelMixin(RevisionMixin):
|
||||
"""
|
||||
A mixin to wrap every request in a revision block.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -5,15 +5,16 @@
|
||||
#
|
||||
# Translators:
|
||||
# Antoine Nguyen <tonio@ngyn.org>, 2020
|
||||
# Nicolas John, 2020
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
|
||||
"POT-Creation-Date: 2020-11-05 15:47+0100\n"
|
||||
"PO-Revision-Date: 2020-05-11 18:45+0000\n"
|
||||
"Last-Translator: Antoine Nguyen <tonio@ngyn.org>, 2020\n"
|
||||
"Last-Translator: Nicolas John, 2020\n"
|
||||
"Language-Team: German (https://www.transifex.com/tonio/teams/13749/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -41,7 +42,7 @@ msgstr "Warnung"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: static/js/global.js:342
|
||||
#: static/js/global.js:347
|
||||
msgid "Internal error"
|
||||
msgstr "Interner Fehler"
|
||||
|
||||
@@ -56,3 +57,11 @@ msgstr "Suche"
|
||||
#: static/js/searchbar.js:28
|
||||
msgid "Search..."
|
||||
msgstr "Suchen..."
|
||||
|
||||
#: static/js/twocols_nav.js:170
|
||||
msgid "Are you sure?"
|
||||
msgstr "Sind Sie sicher?"
|
||||
|
||||
#: static/js/twocols_nav.js:171
|
||||
msgid "This will invalidate your registered application."
|
||||
msgstr "Dadurch wird Ihre registrierte Anwendung ungültig."
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# 李 正 <364109980@qq.com>, 2021
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-11-05 15:47+0100\n"
|
||||
"PO-Revision-Date: 2020-05-11 18:45+0000\n"
|
||||
"Last-Translator: 李 正 <364109980@qq.com>, 2021\n"
|
||||
"Language-Team: Chinese (China) (https://www.transifex.com/tonio/teams/13749/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: zh_CN\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: bower_components/jquery-ui/jquery-ui.min.js:8
|
||||
msgid "'"
|
||||
msgstr "'"
|
||||
|
||||
#: bower_components/swagger-ui/dist/swagger-ui-bundle.js:133
|
||||
msgid "operation"
|
||||
msgstr "操作"
|
||||
|
||||
#: core/static/core/js/settings.js:16
|
||||
msgid "No more log entry to show"
|
||||
msgstr "没有更多的日志可以显示"
|
||||
|
||||
#: static/js/confirmation.js:44
|
||||
msgid "Warning"
|
||||
msgstr "警告"
|
||||
|
||||
#: static/js/confirmation.js:59
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
#: static/js/global.js:347
|
||||
msgid "Internal error"
|
||||
msgstr "内部错误"
|
||||
|
||||
#: static/js/listing.js:21
|
||||
msgid "No more entry to show"
|
||||
msgstr "没有更多的条目可以显示"
|
||||
|
||||
#: static/js/listing.js:46
|
||||
msgid "Search"
|
||||
msgstr "查找"
|
||||
|
||||
#: static/js/searchbar.js:28
|
||||
msgid "Search..."
|
||||
msgstr "查找..."
|
||||
|
||||
#: static/js/twocols_nav.js:170
|
||||
msgid "Are you sure?"
|
||||
msgstr "你确定吗?"
|
||||
|
||||
#: static/js/twocols_nav.js:171
|
||||
msgid "This will invalidate your registered application."
|
||||
msgstr "这会使您的注册申请无效。"
|
||||
@@ -4,10 +4,11 @@ from rest_framework import viewsets
|
||||
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
from modoboa.lib.viewsets import RevisionModelMixin
|
||||
from . import serializers
|
||||
|
||||
|
||||
class RelayDomainViewSet(viewsets.ModelViewSet):
|
||||
class RelayDomainViewSet(RevisionModelMixin, viewsets.ModelViewSet):
|
||||
"""RelayDomain viewset."""
|
||||
|
||||
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
|
||||
|
||||
@@ -1 +1 @@
|
||||
mysqlclient<2.0.2
|
||||
mysqlclient<2.0.4
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ django>=2.2,<2.3
|
||||
django-braces==1.14.0
|
||||
django-ckeditor==6.0.0
|
||||
django-phonenumber-field[phonenumbers]==5.0.0
|
||||
django-reversion==3.0.8
|
||||
django-reversion==3.0.9
|
||||
django-xforwardedfor-middleware==2.0
|
||||
django-webpack-loader
|
||||
django-otp==1.0.2
|
||||
@@ -35,7 +35,7 @@ chardet
|
||||
ovh
|
||||
oath
|
||||
redis
|
||||
rrdtool>=0.1.11
|
||||
rrdtool>=0.1.10
|
||||
qrcode
|
||||
|
||||
aiosmtplib
|
||||
|
||||
@@ -21,3 +21,5 @@ max-line-length = 80
|
||||
[flake8]
|
||||
exclude = migrations
|
||||
max-line-length = 80
|
||||
per-file-ignores =
|
||||
modoboa/core/commands/templates/settings.py.tpl: E265
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
argon2-cffi>=16.1.0
|
||||
factory-boy<=2.12.0
|
||||
httmock==1.4.0
|
||||
testfixtures==6.15.0
|
||||
testfixtures==6.17.1
|
||||
tox
|
||||
asynctest
|
||||
|
||||
Reference in New Issue
Block a user