mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
@@ -33,3 +33,4 @@ test_project/modoboa.db
|
||||
# Misc
|
||||
.ash_history
|
||||
modoboa.kdev4
|
||||
.kateproject
|
||||
|
||||
@@ -131,6 +131,67 @@ Rebuild Virtual Environment
|
||||
Specific instructions
|
||||
*********************
|
||||
|
||||
|
||||
2.2.3
|
||||
=====
|
||||
|
||||
* Reduced the number of call to doveadm
|
||||
|
||||
You need to setup a new worker :
|
||||
|
||||
1. Create a new supervisord config (``/etc/supervisor/conf.d/modoboa-base-worker.conf`` by default) :
|
||||
|
||||
.. sourcecode:: ini
|
||||
|
||||
[program:modoboa-base-worker]
|
||||
autostart=true
|
||||
autorestart=true
|
||||
command={%python env path%} {% manage.py instance path%} rqworker modoboa
|
||||
directory={%modoboa home dir%}
|
||||
user=%{modoboa user}
|
||||
numprocs=1
|
||||
stopsignal=TERM
|
||||
|
||||
``python env path`` : Python executable located in your virtual environment created for modoboa.
|
||||
You will find it here ``/srv/modoboa/venv/bin/python`` by default.
|
||||
``manage.py instance path``: Path to manage.py of your modoboa instance.
|
||||
You will find it here : ``/srv/modoboa/instance/manage.py``by default.
|
||||
``Modoboa home dir``: Home dir of the user running modooba.
|
||||
You will find it here ``/srv/modoboa/`` by default.
|
||||
``modoboa user`` : User managing dkim signing (modoboa by default).
|
||||
|
||||
You can help you with ``/etc/supervisor/conf.d/policyd.conf`` (by default).
|
||||
|
||||
Then restart supervisor. ``#> supervisorctl reread && supervisorctl update`` on Debian.
|
||||
|
||||
2. Update the RQ_QUEUES section bellow the ``#REDIS`` section:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
# RQ
|
||||
RQ_QUEUES = {
|
||||
...
|
||||
},
|
||||
'modoboa': {
|
||||
'HOST': REDIS_HOST,
|
||||
'PORT': REDIS_PORT,
|
||||
'DB': 0,
|
||||
},
|
||||
}
|
||||
|
||||
3. Add the CACHE section bellow the ``# RQ`` one:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
# CACHE
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": 'django.core.cache.backends.redis.RedisCache',
|
||||
"LOCATION": REDIS_URL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2.2.0
|
||||
=====
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ services:
|
||||
DEBUG: "True"
|
||||
REDIS_HOST: redis
|
||||
command: >
|
||||
sh -c "cd test_project; python3 manage.py rqworker dkim"
|
||||
sh -c "cd test_project; python3 manage.py rqworker dkim modoboa"
|
||||
volumes:
|
||||
- .:/code
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.9-alpine as base
|
||||
FROM python:3.12-alpine as base
|
||||
|
||||
MAINTAINER Antoine Nguyen <tonio@ngyn.org>
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
@@ -17,8 +17,10 @@ RUN python -m pip install -U pip
|
||||
RUN pip install -r requirements.txt -r test-requirements.txt
|
||||
|
||||
FROM base as run
|
||||
COPY docker/doveadm /usr/local/bin
|
||||
RUN chmod +x /usr/local/bin/doveadm
|
||||
COPY --from=build $VIRTUAL_ENV $VIRTUAL_ENV
|
||||
RUN apk add --no-cache --update openssl librrd libxslt libjpeg libressl git rrdtool libmagic \
|
||||
RUN apk add --no-cache --update openssl librrd libxslt libjpeg libressl git rrdtool libmagic sudo \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
RUN mkdir /code
|
||||
WORKDIR /code
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
FROM node:16.10-alpine
|
||||
FROM node:16.10-alpine
|
||||
|
||||
ENV HOME=/home/node/
|
||||
|
||||
RUN apk add --no-cache --update gettext \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
WORKDIR $HOME
|
||||
|
||||
RUN yarn install
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "SHA1 SSHA512 SCRAM-SHA-256 BLF-CRYPT PLAIN HMAC-MD5 OTP SHA512 SHA DES-CRYPT CRYPT SSHA MD5-CRYPT PLAIN-MD4 PLAIN-MD5 SCRAM-SHA-1 SHA512-CRYPT CLEAR CLEARTEXT ARGON2I ARGON2ID SSHA256 MD5 PBKDF2 SHA256 CRAM-MD5 PLAIN-TRUNC SHA256-CRYPT SMD5 DIGEST-MD5 LDAP-MD5"
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
"debounce": "^1.2.1",
|
||||
"js-cookie": "^2.2.1",
|
||||
"luxon": "^1.28.1",
|
||||
"qrcode.vue": "~1.7.0",
|
||||
"vee-validate": "^3.4.5",
|
||||
"vue": "^2.6.14",
|
||||
"vue-apexcharts": "^1.6.0",
|
||||
@@ -23,8 +24,7 @@
|
||||
"vue-gettext": "^2.1.12",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.6.10",
|
||||
"vuex": "^3.4.0",
|
||||
"qrcode.vue": "~1.7.0"
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
|
||||
+3
-3
@@ -2517,9 +2517,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001407:
|
||||
version "1.0.30001481"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz"
|
||||
integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==
|
||||
version "1.0.30001553"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz"
|
||||
integrity sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==
|
||||
|
||||
case-sensitive-paths-webpack-plugin@^2.3.0:
|
||||
version "2.4.0"
|
||||
|
||||
@@ -75,6 +75,8 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
@mock.patch("socket.getaddrinfo")
|
||||
def test_create_domain_with_mx_check(self, mock_getaddrinfo, mock_query):
|
||||
"""Check domain creation when MX check is activated."""
|
||||
mock_query.side_effect = utils.mock_dns_query_result
|
||||
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
||||
self.set_global_parameter("enable_admin_limits", False, app="limits")
|
||||
self.set_global_parameter("valid_mxs", "192.0.2.1 2001:db8::1")
|
||||
self.set_global_parameter("domains_must_have_authorized_mx", True)
|
||||
@@ -84,8 +86,6 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
|
||||
|
||||
url = reverse("v1:domain-list")
|
||||
mock_query.side_effect = utils.mock_dns_query_result
|
||||
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
||||
response = self.client.post(
|
||||
url, {"name": "no-mx.example.com", "quota": 0,
|
||||
"default_mailbox_quota": 10}
|
||||
|
||||
@@ -210,6 +210,9 @@ class DomainTestCase(ModoTestCase):
|
||||
@mock.patch("socket.getaddrinfo")
|
||||
def test_create_and_check_mx(self, mock_getaddrinfo, mock_query):
|
||||
"""Check for authorized MX record."""
|
||||
mock_query.side_effect = utils.mock_dns_query_result
|
||||
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
||||
|
||||
reseller = core_factories.UserFactory(
|
||||
username="reseller", groups=("Resellers", ))
|
||||
self.client.force_login(reseller)
|
||||
@@ -218,8 +221,6 @@ class DomainTestCase(ModoTestCase):
|
||||
self.set_global_parameter("valid_mxs", "192.0.2.1 2001:db8::1")
|
||||
self.set_global_parameter("domains_must_have_authorized_mx", True)
|
||||
|
||||
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,
|
||||
|
||||
@@ -83,14 +83,14 @@ domainalias; domalias1.com; domain1.com; True
|
||||
@mock.patch("socket.getaddrinfo")
|
||||
def test_domain_import_with_mx_check(self, mock_getaddrinfo, mock_query):
|
||||
"""Check domain import when MX check is enabled."""
|
||||
mock_query.side_effect = utils.mock_dns_query_result
|
||||
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
||||
reseller = core_factories.UserFactory(
|
||||
username="reseller", groups=("Resellers", ))
|
||||
self.client.force_login(reseller)
|
||||
self.set_global_parameter("valid_mxs", "192.0.2.1 2001:db8::1")
|
||||
self.set_global_parameter("domains_must_have_authorized_mx", True)
|
||||
|
||||
mock_query.side_effect = utils.mock_dns_query_result
|
||||
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
||||
f = ContentFile(
|
||||
b"domain; test3.com; 100; 1; True", name="domains.csv")
|
||||
resp = self.client.post(
|
||||
|
||||
@@ -36,6 +36,7 @@ _DNE_MX_RECORD = MX("IN", "MX", 10, Name(
|
||||
_MX_RECORDS = [_MX_RECORD_1]
|
||||
_IP_SET_RECORDS = [RRset()]
|
||||
|
||||
_IPV4_RECORD_REDIS = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", int(settings.REDIS_PORT)))
|
||||
_IPV4_RECORD_1 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", 25))
|
||||
_IPV4_RECORD_2 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.2", 25))
|
||||
_IPV6_RECORD = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("2001:db8::1", 25))
|
||||
@@ -69,7 +70,7 @@ _POSSIBLE_DNS_RESULTS_NO_MX = {
|
||||
"mx3.example.com": [_A_RECORD]
|
||||
}
|
||||
_POSSIBLE_IP_RESULTS = {
|
||||
settings.REDIS_HOST: socket.getaddrinfo(settings.REDIS_HOST, settings.REDIS_PORT),
|
||||
settings.REDIS_HOST: [_IPV4_RECORD_REDIS],
|
||||
"test3.com": [_IPV4_RECORD_2],
|
||||
"mx2.example.com": [_IPV4_RECORD_2],
|
||||
"bad-response.example.com": [_BAD_IP_RECORD],
|
||||
|
||||
@@ -21,7 +21,9 @@ from django.utils.translation import gettext_lazy, gettext as _
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from modoboa.core import constants, models, sms_backends, app_settings, context_processors
|
||||
from modoboa.core import (
|
||||
constants, models, sms_backends, app_settings, context_processors
|
||||
)
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib import fields as lib_fields, cryptutils
|
||||
|
||||
@@ -62,21 +64,15 @@ class CoreGlobalParametersSerializer(serializers.Serializer):
|
||||
default="local"
|
||||
)
|
||||
password_scheme = serializers.ChoiceField(
|
||||
choices=[("sha512crypt", "sha512crypt"),
|
||||
("sha256crypt", "sha256crypt"),
|
||||
("blfcrypt", "bcrypt"),
|
||||
("md5crypt", gettext_lazy("md5crypt (weak)")),
|
||||
("sha256", gettext_lazy("sha256 (weak)")),
|
||||
("md5", gettext_lazy("md5 (weak)")),
|
||||
("crypt", gettext_lazy("crypt (weak)")),
|
||||
("plain", gettext_lazy("plain (weak)"))],
|
||||
default="sha512crypt"
|
||||
choices=[("sha512crypt", "sha512crypt")],
|
||||
required=False
|
||||
)
|
||||
rounds_number = serializers.IntegerField(default=70000)
|
||||
update_scheme = serializers.BooleanField(default=True)
|
||||
default_password = serializers.CharField(default="password")
|
||||
default_password = serializers.CharField(default="ChangeMe1!")
|
||||
random_password_length = serializers.IntegerField(min_value=8, default=8)
|
||||
update_password_url = serializers.URLField(required=False, allow_blank=True)
|
||||
update_password_url = serializers.URLField(
|
||||
required=False, allow_blank=True)
|
||||
password_recovery_msg = serializers.CharField(
|
||||
required=False, allow_blank=True)
|
||||
sms_password_recovery = serializers.BooleanField(default=False)
|
||||
@@ -175,6 +171,8 @@ class CoreGlobalParametersSerializer(serializers.Serializer):
|
||||
**definition["attrs"])
|
||||
# Choices serializer for default_top_redirection field
|
||||
self.fields["default_top_redirection"].choices = app_settings.enabled_applications()
|
||||
# Populate choices of password_scheme
|
||||
self.fields["password_scheme"].choices = app_settings.get_password_scheme()
|
||||
|
||||
def validate_ldap_user_dn_template(self, value):
|
||||
try:
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
"""Core API related tests."""
|
||||
|
||||
import copy
|
||||
import getpass
|
||||
import oath
|
||||
from unittest import mock
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django_rq import get_worker, get_queue
|
||||
|
||||
from modoboa.admin import factories
|
||||
from modoboa.core import models
|
||||
from modoboa.core.tests import utils
|
||||
from modoboa.lib.tests import ModoAPITestCase
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
DOVEADM_TEST_PATH = utils.get_doveadm_test_path()
|
||||
DOVECOT_USER = getpass.getuser()
|
||||
|
||||
CORE_SETTINGS = {
|
||||
"authentication_type": "local",
|
||||
"password_scheme": "sha512crypt",
|
||||
@@ -70,12 +78,23 @@ CORE_SETTINGS = {
|
||||
|
||||
class ParametersAPITestCase(ModoAPITestCase):
|
||||
|
||||
@override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH],
|
||||
DOVECOT_USER=DOVECOT_USER)
|
||||
def test_update(self):
|
||||
cache.delete("password_scheme_choice")
|
||||
queue = get_queue("modoboa", is_async=False)
|
||||
queue.empty()
|
||||
|
||||
url = reverse("v2:parameter-detail", args=["core"])
|
||||
data = copy.copy(CORE_SETTINGS)
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
get_worker("modoboa").work(burst=True)
|
||||
self.assertIsNotNone(cache.get("password_scheme_choice"))
|
||||
self.assertIn(('plain', 'plain (weak)'),
|
||||
cache.get("password_scheme_choice")
|
||||
)
|
||||
# Modify SMS related settings
|
||||
data["sms_password_recovery"] = True
|
||||
resp = self.client.put(url, data, format="json")
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
import collections
|
||||
|
||||
from django import forms
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import password_validation
|
||||
from django.utils.translation import gettext as _, gettext_lazy
|
||||
|
||||
from django_rq import get_queue
|
||||
|
||||
from modoboa.core.jobs import job_retrieve_available_hashers
|
||||
from modoboa.core.password_hashers import get_dovecot_schemes
|
||||
from modoboa.core.password_hashers.base import PasswordHasher
|
||||
from modoboa.lib import fields as lib_fields
|
||||
@@ -31,6 +35,23 @@ def enabled_applications():
|
||||
return sorted(result, key=lambda e: e[0])
|
||||
|
||||
|
||||
def get_default_password_scheme():
|
||||
available_schemes = get_dovecot_schemes()
|
||||
return [
|
||||
(hasher.name, gettext_lazy(hasher.label))
|
||||
for hasher in PasswordHasher.get_password_hashers()
|
||||
if hasher().scheme in available_schemes
|
||||
]
|
||||
|
||||
|
||||
def get_password_scheme():
|
||||
available_schemes = cache.get("password_scheme_choice")
|
||||
if available_schemes is None:
|
||||
get_queue("modoboa").enqueue(job_retrieve_available_hashers)
|
||||
return get_default_password_scheme()
|
||||
return available_schemes
|
||||
|
||||
|
||||
class GeneralParametersForm(param_forms.AdminParametersForm):
|
||||
"""General parameters."""
|
||||
|
||||
@@ -73,7 +94,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
|
||||
|
||||
default_password = forms.CharField(
|
||||
label=gettext_lazy("Default password"),
|
||||
initial="password",
|
||||
initial="ChangeMe1!",
|
||||
help_text=gettext_lazy(
|
||||
"Default password for automatically created accounts.")
|
||||
)
|
||||
@@ -535,12 +556,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
|
||||
self._add_visibilty_rules(
|
||||
sms_backends.get_all_backend_visibility_rules()
|
||||
)
|
||||
available_schemes = get_dovecot_schemes()
|
||||
self.fields["password_scheme"].choices = [
|
||||
(hasher.name, gettext_lazy(hasher.label))
|
||||
for hasher in PasswordHasher.get_password_hashers()
|
||||
if hasher().scheme in available_schemes
|
||||
]
|
||||
self.fields["password_scheme"].choices = get_password_scheme()
|
||||
|
||||
def _add_dynamic_fields(self):
|
||||
new_fields = collections.OrderedDict()
|
||||
|
||||
@@ -262,8 +262,23 @@ RQ_QUEUES = {
|
||||
'PORT': REDIS_PORT,
|
||||
'DB': 0,
|
||||
},
|
||||
'modoboa': {
|
||||
'HOST': REDIS_HOST,
|
||||
'PORT': REDIS_PORT,
|
||||
'DB': 0,
|
||||
},
|
||||
}
|
||||
|
||||
# CACHE
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": 'django.core.cache.backends.redis.RedisCache',
|
||||
"LOCATION": REDIS_URL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from rq import get_current_job
|
||||
|
||||
from modoboa.core.password_hashers import cache_available_password_hasher
|
||||
|
||||
|
||||
def job_retrieve_available_hashers():
|
||||
if get_current_job() is None:
|
||||
return
|
||||
cache_available_password_hasher()
|
||||
@@ -1,7 +1,6 @@
|
||||
# Generated by Django 1.9.5 on 2016-06-15 18:12
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -17,7 +16,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('api_pk', models.PositiveIntegerField(null=True)),
|
||||
('api_versions', jsonfield.fields.JSONField()),
|
||||
('api_versions', models.JSONField()),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Generated by Django 1.9.5 on 2016-10-26 08:11
|
||||
from django.db import migrations
|
||||
import jsonfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_parameter(store, parameter):
|
||||
@@ -51,13 +50,13 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='localconfig',
|
||||
name='_parameters',
|
||||
field=jsonfield.fields.JSONField(default={}),
|
||||
field=models.JSONField(default=dict),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='_parameters',
|
||||
field=jsonfield.fields.JSONField(default={}),
|
||||
field=models.JSONField(default=dict),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(move_parameters, clear_parameters)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Generated by Django 1.9.5 on 2016-11-04 21:17
|
||||
from django.db import migrations
|
||||
import jsonfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -13,11 +12,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='localconfig',
|
||||
name='_parameters',
|
||||
field=jsonfield.fields.JSONField(default={}),
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='_parameters',
|
||||
field=jsonfield.fields.JSONField(default={}),
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-24 17:14
|
||||
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0025_rename_user_email_is_active_core_user_email_c0c03f_idx'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='localconfig',
|
||||
name='_parameters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='localconfig',
|
||||
name='api_versions',
|
||||
field=models.JSONField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='_parameters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='localconfig',
|
||||
name='api_versions',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='localconfig',
|
||||
name='_parameters',
|
||||
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='localconfig',
|
||||
name='api_versions',
|
||||
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='_parameters',
|
||||
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
]
|
||||
@@ -3,6 +3,7 @@
|
||||
import re
|
||||
from email.header import Header
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@@ -14,7 +15,6 @@ from django.contrib.auth.models import AbstractUser, Group
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
import jsonfield
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from reversion import revisions as reversion
|
||||
|
||||
@@ -74,7 +74,7 @@ class User(AbstractUser):
|
||||
|
||||
tfa_enabled = models.BooleanField(default=False)
|
||||
|
||||
_parameters = jsonfield.JSONField(default={})
|
||||
_parameters = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
|
||||
|
||||
class Meta(object):
|
||||
ordering = ["username"]
|
||||
@@ -447,9 +447,9 @@ class LocalConfig(models.Model):
|
||||
site = models.ForeignKey("sites.Site", on_delete=models.CASCADE)
|
||||
|
||||
# API results cache
|
||||
api_versions = jsonfield.JSONField()
|
||||
api_versions = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
|
||||
|
||||
_parameters = jsonfield.JSONField(default={})
|
||||
_parameters = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
|
||||
|
||||
# Dovecot LDAP update
|
||||
need_dovecot_update = models.BooleanField(default=False)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Password hashers for Modoboa.
|
||||
"""
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
@@ -10,7 +11,7 @@ from modoboa.core.password_hashers.advanced import ( # NOQA:F401
|
||||
ARGON2IDHasher, SSHAHasher
|
||||
)
|
||||
from modoboa.core.password_hashers.base import ( # NOQA:F401
|
||||
CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher
|
||||
CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher, PasswordHasher
|
||||
)
|
||||
from modoboa.lib.sysutils import doveadm_cmd
|
||||
|
||||
@@ -55,3 +56,15 @@ def get_dovecot_schemes():
|
||||
|
||||
return ["{{{}}}".format(smart_str(scheme))
|
||||
for scheme in schemes.split()]
|
||||
|
||||
|
||||
def cache_available_password_hasher():
|
||||
available_schemes = get_dovecot_schemes()
|
||||
password_scheme_choice = [
|
||||
(hasher.name, hasher.label)
|
||||
for hasher in PasswordHasher.get_password_hashers()
|
||||
if hasher().scheme in available_schemes
|
||||
]
|
||||
cache.set("password_scheme_choice",
|
||||
password_scheme_choice,
|
||||
2592000)
|
||||
|
||||
@@ -233,12 +233,12 @@ class SettingsTestCase(ModoTestCase):
|
||||
settings = SETTINGS_SAMPLE.copy()
|
||||
response = self.client.post(url, settings, format="json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
settings["core-password_scheme"] = "sha512crypt"
|
||||
settings["core-password_scheme"] = "magic_password_scheme"
|
||||
response = self.client.post(url, settings, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
compare(response.json(), {
|
||||
"form_errors": {"password_scheme": ["Select a valid choice. \
|
||||
sha512crypt is not one of the available choices."]},
|
||||
magic_password_scheme is not one of the available choices."]},
|
||||
"prefix": "core"
|
||||
})
|
||||
settings["core-password_scheme"] = "plain"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import os
|
||||
|
||||
def get_doveadm_test_path():
|
||||
return "{}/doveadm".format(os.path.dirname(__file__))
|
||||
@@ -1,8 +1,6 @@
|
||||
# Generated by Django 1.10.7 on 2017-11-24 11:16
|
||||
from django.db import migrations, models
|
||||
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -19,7 +17,7 @@ class Migration(migrations.Migration):
|
||||
('pattern', models.CharField(max_length=254, unique=True, verbose_name='pattern')),
|
||||
('service', models.CharField(max_length=30, verbose_name='service')),
|
||||
('next_hop', models.CharField(blank=True, max_length=100, verbose_name='next hop')),
|
||||
('_settings', jsonfield.fields.JSONField(default={})),
|
||||
('_settings', models.JSONField(default=dict)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['pattern'],
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-24 17:14
|
||||
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transport', '0002_auto_20180928_1520'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='transport',
|
||||
name='_settings',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transport',
|
||||
name='_settings',
|
||||
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
),
|
||||
]
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Transport models."""
|
||||
|
||||
import jsonfield
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -17,7 +17,7 @@ class Transport(models.Model):
|
||||
service = models.CharField(_("service"), max_length=30)
|
||||
next_hop = models.CharField(_("next hop"), max_length=100, blank=True)
|
||||
|
||||
_settings = jsonfield.JSONField(default={})
|
||||
_settings = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
|
||||
|
||||
class Meta:
|
||||
ordering = ["pattern"]
|
||||
|
||||
@@ -21,7 +21,6 @@ asgiref
|
||||
dnspython==2.4.2
|
||||
feedparser==6.0.11
|
||||
gevent==23.9.1
|
||||
jsonfield==3.1.0
|
||||
Pillow # Optional dependency for django-ckeditor
|
||||
progressbar33==2.4
|
||||
python-dateutil
|
||||
|
||||
@@ -225,6 +225,8 @@ SPECTACULAR_SETTINGS = {
|
||||
# MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png")
|
||||
|
||||
# DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', )
|
||||
DOVECOT_USER="root"
|
||||
|
||||
|
||||
MODOBOA_API_URL = 'https://api.modoboa.org/1/'
|
||||
|
||||
@@ -243,6 +245,18 @@ RQ_QUEUES = {
|
||||
'dkim': {
|
||||
'URL': REDIS_URL,
|
||||
},
|
||||
'modoboa': {
|
||||
'URL': REDIS_URL,
|
||||
},
|
||||
}
|
||||
|
||||
# CACHE
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": 'django.core.cache.backends.redis.RedisCache',
|
||||
"LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}"
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
|
||||
Reference in New Issue
Block a user