diff --git a/doc/installation.rst b/doc/installation.rst index 15b07506e..aa8a3c877 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -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 ---------- diff --git a/doc/manual_installation/radicale.rst b/doc/manual_installation/radicale.rst new file mode 100644 index 000000000..cc53aa94f --- /dev/null +++ b/doc/manual_installation/radicale.rst @@ -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 + > 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:@/api/o/introspect/ + + +Replace ```` with the value you obtained earlier. diff --git a/doc/upgrade.rst b/doc/upgrade.rst index 9adc118f7..7b9009e3c 100644 --- a/doc/upgrade.rst +++ b/doc/upgrade.rst @@ -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 -i bash - > cd - > cp frontend/config.json ./ - > rm -rf frontend - > cp -R /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 `. + 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/` 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 diff --git a/modoboa/core/checks.py b/modoboa/core/checks.py index ca61a40af..c12101719 100644 --- a/modoboa/core/checks.py +++ b/modoboa/core/checks.py @@ -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 diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index deda33932..be4e73a82 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -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"""{{ diff --git a/modoboa/core/tests/test_checks.py b/modoboa/core/tests/test_checks.py index 65e1fc8c1..d2fe5df1e 100644 --- a/modoboa/core/tests/test_checks.py +++ b/modoboa/core/tests/test_checks.py @@ -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) diff --git a/modoboa/static/css/favicon.ico b/modoboa/static/css/favicon.ico new file mode 100644 index 000000000..985be6b03 Binary files /dev/null and b/modoboa/static/css/favicon.ico differ diff --git a/modoboa/static/css/logo-icon.png b/modoboa/static/css/logo-icon.png deleted file mode 100644 index 859251fe0..000000000 Binary files a/modoboa/static/css/logo-icon.png and /dev/null differ diff --git a/modoboa/templates/layout_simple.html b/modoboa/templates/layout_simple.html index f0378811e..4a9ebc701 100644 --- a/modoboa/templates/layout_simple.html +++ b/modoboa/templates/layout_simple.html @@ -6,7 +6,7 @@ {% block pagetitle %}{% endblock %} - + diff --git a/modoboa/templates/registration/base.html b/modoboa/templates/registration/base.html index 12feed341..bc41ca789 100644 --- a/modoboa/templates/registration/base.html +++ b/modoboa/templates/registration/base.html @@ -6,7 +6,7 @@ {% trans "Welcome to Modoboa" %} - + {% block extra_css %}{% endblock %} diff --git a/pyproject.toml b/pyproject.toml index 7b37e13e4..fb2af69d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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",