Merge pull request #3593 from modoboa/fix/for.2.4.2

Some fixes for 2.4.1
This commit is contained in:
Antoine Nguyen
2025-07-18 12:47:12 +02:00
committed by GitHub
11 changed files with 176 additions and 40 deletions
+1
View File
@@ -72,6 +72,7 @@ specific part, here are the steps you must follow:
manual_installation/dovecot
manual_installation/postfix
manual_installation/opendkim
manual_installation/radicale
Extensions
----------
+57
View File
@@ -0,0 +1,57 @@
.. _radicale:
#######
Radicale
#######
This page is currently being written.
.. _radicale_oauth2:
OAuth 2 authentication
======================
.. note::
These steps are automatic if you use an up-to-date modoboa-installer to install/upgrade.
Installing radicale oauth plugin
--------------------------------
You need to install ``radicale-modoboa-auth-oauth2`` python package inside radicale venv.
If you used the installer you can use the following commands:
.. sourcecode:: bash
> sudo -u radicale -i bash
> cd ~
> env/bin/pip install radicale-modoboa-auth-oauth2
Oauth application creation
--------------------------
You first need to register Radicale as an authorized consumer of the
OAuth2 authentication service provided by Modoboa. To do so, create an
application with the following commands:
.. sourcecode:: bash
> cd <modoboa_instance_path>
> python manage.py createapplication --name=Radicale --skip-authorization --client-id=radicale confidential client-credentials
On success, you should see an output similar to::
New application Radicale created successfully.
client_secret: XXXX
To enable OAuth2 authentication in Radicale, edit the :file:`/etc/radicale/config`
file and update the `type` in `[auth]` to::
type = radicale_modoboa_auth_oauth2
Then, add the following line in `[auth]`::
oauth2_introspection_endpoint = https://radical:<client_secret>@<hostname of your server>/api/o/introspect/
Replace ``<client_secret>`` with the value you obtained earlier.
+12 -21
View File
@@ -85,24 +85,6 @@ If some error appears, you may try this:
> git reset --hard origin/master
New-admin interface
*******************
.. note::
You don't need to perform these actions if you use the installer to upgrade your instance.
New admin interface won't update by itself. Run the following commands to update it:
.. sourcecode:: bash
> sudo -u <modoboa_user> -i bash
> cd <modoboa_instance_dir>
> cp frontend/config.json ./
> rm -rf frontend
> cp -R <virtuenv_path>/lib/pythonX.X/site-packages/modoboa/frontend_dist/ frontend/
> cp ./config.json frontend/config.json
Extensions
**********
@@ -195,8 +177,9 @@ Required changes to :file:`settings.py`
- ``modoboa_contacts`` -> ``modoboa.contacts``
- ``modoboa_radicale`` -> ``modoboa.calendars``
- ``modoboa_webmail`` -> ``modoboa.webmail``
- ``modoboa_rspamd`` -> ``modoboa.rspamd``
- Remove configuration blocks related to ``modoboa_contacts`` and ``modoboa_radicale`` plugins
- Remove configuration blocks related to ``modoboa_contacts`` and ``modoboa_radicale`` plugins (these should be at the end of the file)
- Remove ``AjaxLoginRedirect`` from ``MIDDLEWARE`` variable:
@@ -217,14 +200,20 @@ Required changes to :file:`settings.py`
'modoboa.lib.middleware.RequestCatcherMiddleware',
)
- Remove ``NEW_ADMIN_URL`` variable
- Remove ``NEW_ADMIN_URL`` variable if found
- Remove ``new_admin_url`` and ``top_notifications`` context processors
- Remove ``ckeditor`` related apps from ``INSTALLED_APPS`` variable
- Remove ``ckeditor`` related apps from ``INSTALLED_APPS`` variable and ``CKEDITOR_*`` settings
- Remove ``STATICFILE_DIRS`` variable
Radicale configuration changes
-----------------------------
You must enable oauth2 authentication in Radicale to make the calendar
and contact feature work. Follow this :ref:`guide <radicale_oauth2>`.
Post update
-----------
@@ -236,6 +225,8 @@ Post update
- Remove ``autoreply`` service definition from postfix`s :file:`master.cf` file if you were using it
- Make sure :file:`/etc/uwsgi/sites-enabled/<modoboa_conf_file>` contains `max-requests=5000` and that `process` is superior or equal to 4. Don't forget to restart uwsgi service after the configuration change.
- Then proceed with regular upgrade instructions
Version 2.3.3
+48 -3
View File
@@ -1,8 +1,10 @@
from django.db.utils import ProgrammingError, OperationalError
from django.core.checks import register, Info, Warning
from django.core.checks import register, Info, Warning, Error
from django.conf import settings
from django.utils.translation import gettext as _
from oauth2_provider.models import get_application_model
from modoboa.core.utils import (
check_for_deprecated_password_schemes,
generate_rsa_private_key,
@@ -15,14 +17,41 @@ W001 = Warning(
"twice due to clock change)."
),
hint=_("Set `USE_TZ = True` in settings.py"),
id="modoboa.W001",
id="modoboa.core.W001",
)
W002 = Warning(
_("The password scheme you are using is deprecated."),
hint=_("Upgrade your password scheme to a stronger one"),
id="modoboa.W002",
id="modoboa.core.W002",
)
E001 = Error(
_(
"Oauth2 application for dovecot has not been found."
" Webmail and some other features won't be available."
),
hint=_(
"Visit : https://modoboa.readthedocs.io/en/latest/manual_installation/dovecot.html#dovecot-oauth2"
),
id="modoboa.core.E001",
)
E002 = Error(
_(
"Oauth2 application for radicale has not been found."
" Calendar and some other features won't be available."
),
hint=_(
"Visit : https://modoboa.readthedocs.io/en/latest/manual_installation/radicale.html#radicale-oauth2"
),
id="modoboa.core.E002",
)
def oauth_app_exists(app_name):
oauth_application = get_application_model().objects.filter(name=app_name)
return oauth_application.exists()
@register(deploy=True)
def check_use_tz_enabled(app_configs, **kwargs):
@@ -59,3 +88,19 @@ def check_password_hasher(app_configs, **kwargs):
if hasher:
msgs.append(W002)
return msgs
@register(deploy=True)
def check_dovecot_oauth_app(*args, **kwargs):
error = []
if not oauth_app_exists("Dovecot"):
error.append(E001)
return error
@register(deploy=True)
def check_radicale_oauth_app(*args, **kwargs):
error = []
if not oauth_app_exists("Radicale"):
error.append(E002)
return error
@@ -106,15 +106,18 @@ class Command(BaseCommand):
app_model = get_application_model()
allowed_host = getattr(settings, "ALLOWED_HOSTS", None)
if allowed_host is not None:
allowed_host = allowed_host[0]
else:
if allowed_host is None:
allowed_host = input("What will be the hostname used to access Modoboa? ")
if not allowed_host:
allowed_host = "localhost"
allowed_host = [allowed_host]
frontend_application = app_model.objects.filter(name="modoboa_frontend")
base_uri = f"https://{allowed_host}"
redirect_uri = f"{base_uri}/login/logged"
# TODO : improve support for multiple allowed_host for frontend
base_uris_list = [f"https://{host}" for host in allowed_host]
base_uris = " ".join(base_uris_list)
base_uri = base_uris_list[0]
redirect_uris = " ".join([f"{uri}/login/logged" for uri in base_uris_list])
redirect_uri = redirect_uris.split(" ")[0]
client_id = ""
if not frontend_application.exists():
if options["dev"]:
@@ -126,18 +129,18 @@ class Command(BaseCommand):
call_command(
"createapplication",
"--algorithm=RS256",
f"--redirect-uris={redirect_uri}",
f"--redirect-uris={redirect_uris}",
"--name=modoboa_frontend",
f"--client-id={client_id}",
f"--post-logout-redirect-uris={base_uri}",
f"--post-logout-redirect-uris={base_uris}",
"--skip-authorization",
"public",
"authorization-code",
)
else:
app = frontend_application.first()
app.redirect_uris = redirect_uri
app.post_logout_redirect_urls = base_uri
app.redirect_uris = redirect_uris
app.post_logout_redirect_uris = base_uris
app.save()
client_id = app.client_id
@@ -150,9 +153,11 @@ class Command(BaseCommand):
os.makedirs(frontend_target_dir, exist_ok=True)
for entry in os.scandir(base_frontend_dir):
if entry.name != "config.json":
os.symlink(f"{base_frontend_dir}/{entry.name}",
f"{frontend_target_dir}/{entry.name}",
target_is_directory=entry.is_dir())
os.symlink(
f"{base_frontend_dir}/{entry.name}",
f"{frontend_target_dir}/{entry.name}",
target_is_directory=entry.is_dir(),
)
with open(f"{frontend_target_dir}/config.json", "w") as fp:
fp.write(
f"""{{
+38 -1
View File
@@ -1,15 +1,18 @@
import os
import shutil
import tempfile
import uuid
from django.core.management import call_command
from django.test.utils import override_settings
from oauth2_provider.models import get_application_model
from modoboa.core import checks
from modoboa.lib.tests import SimpleModoTestCase
class CheckSessionCookieSecureTest(SimpleModoTestCase):
def setUp(self):
self.workdir = tempfile.mkdtemp()
@@ -35,3 +38,37 @@ class CheckSessionCookieSecureTest(SimpleModoTestCase):
os.environ["OIDC_RSA_PRIVATE_KEY"] = "KEY"
msgs = checks.check_rsa_private_key_exists(None)
self.assertEqual(len(msgs), 0)
def test_dovecot_oauth_app_exists(self):
get_application_model().objects.filter(name="Dovecot").delete()
msgs = checks.check_dovecot_oauth_app()
self.assertEqual(len(msgs), 1)
client_secret = str(uuid.uuid4())
call_command(
"createapplication",
"--name=Dovecot",
"--skip-authorization",
"--client-id=dovecot",
f"--client-secret={client_secret}",
"confidential",
"client-credentials",
)
msgs = checks.check_dovecot_oauth_app()
self.assertEqual(len(msgs), 0)
def test_radicale_oauth_app_exists(self):
get_application_model().objects.filter(name="Radicale").delete()
msgs = checks.check_radicale_oauth_app()
self.assertEqual(len(msgs), 1)
client_secret = str(uuid.uuid4())
call_command(
"createapplication",
"--name=Radicale",
"--skip-authorization",
"--client-id=radicale",
f"--client-secret={client_secret}",
"confidential",
"client-credentials",
)
msgs = checks.check_radicale_oauth_app()
self.assertEqual(len(msgs), 0)
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 B

+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>{% block pagetitle %}{% endblock %}</title>
<link rel="icon" type="image/png" href="{% static 'css/logo-icon.png' %}" />
<link rel="icon" type="image/x-icon" href="{% static 'css/favicon.ico' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/offline.css' %}" />
</head>
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>{% trans "Welcome to Modoboa" %}</title>
<link rel="icon" type="image/png" href="{% static 'css/logo-icon.png' %}" />
<link rel="icon" type="image/x-icon" href="{% static 'css/favicon.ico' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/offline.css' %}" />
{% block extra_css %}{% endblock %}
</head>
+1 -1
View File
@@ -37,7 +37,7 @@ dependencies = [
"django-rename-app==0.1.7",
"django-environ",
"dj-database-url",
"djangorestframework>=3.14,<3.17",
"djangorestframework>=3.16,<3.17",
"drf-spectacular",
"uritemplate",
"django-oauth-toolkit",