diff --git a/doc/upgrade.rst b/doc/upgrade.rst index 06e648e06..99a755976 100644 --- a/doc/upgrade.rst +++ b/doc/upgrade.rst @@ -28,6 +28,13 @@ use them: > sudo -u -i bash > source /bin/activate + +.. _post_upgrade_commands: + +.. note:: + + Check the :ref:`specific_upgrade_instructions` before running the following commands. + Then, run the following commands: .. sourcecode:: bash @@ -37,6 +44,7 @@ Then, run the following commands: > python manage.py migrate > python manage.py collectstatic > python manage.py check --deploy + > python manage.py load_initial_data Once done, check if the version you are installing requires :ref:`specific_upgrade_instructions`. @@ -146,8 +154,8 @@ Version 2.3.0 Pre update ---------- - -Before your start upgrading modoboa, run the following commands from +@TODO : Update this with SQL changes, clarify "before you start upgrading modoboa" +Before you start upgrading modoboa, run the following commands from your virtual environment: .. sourcecode:: bash @@ -259,7 +267,21 @@ by default. 'modoboa.lib.middleware.RequestCatcherMiddleware', ) -- Add the following content after ``MEDIA_ROOT``: +- Add ``'oauth2_provider.contrib.rest_framework.OAuth2Authentication'`` + at the top of the list of ``DEFAULT_AUTHENTICATION_CLASSES`` + (inside ``REST_FRAMEWORK`` section): + + .. sourcecode:: python + + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + + +- Run the commands indicated :ref:`in the first section ` + then add the following content after ``MEDIA_ROOT``: .. sourcecode:: python @@ -282,19 +304,6 @@ by default. # If CORS fail, you might want to try to set it to True #CORS_ORIGIN_ALLOW_ALL = False -- Add ``'oauth2_provider.contrib.rest_framework.OAuth2Authentication'`` - at the top of the list of ``DEFAULT_AUTHENTICATION_CLASSES`` - (inside ``REST_FRAMEWORK`` section): - - .. sourcecode:: python - - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ), - - SORBS DNS --------- @@ -983,7 +992,7 @@ This release introduces an optional LDAP synchronization process. If you want to use it, please follow the :ref:`dedicated procedure `. Version 1.13.1 -======= +============== Upgrade postfix maps files as follows: diff --git a/modoboa/core/checks/settings_checks.py b/modoboa/core/checks.py similarity index 63% rename from modoboa/core/checks/settings_checks.py rename to modoboa/core/checks.py index f72969226..fb445a9e7 100644 --- a/modoboa/core/checks/settings_checks.py +++ b/modoboa/core/checks.py @@ -1,7 +1,10 @@ +from django.core.checks import register, Info, Warning from django.conf import settings -from django.core.checks import Warning, register from django.utils.translation import gettext as _ +from modoboa.core.utils import generate_rsa_private_key + + W001 = Warning( _( "You have USE_TZ set to False, this may result in issues during " @@ -24,3 +27,14 @@ def check_use_tz_enabled(app_configs, **kwargs): if not settings.USE_TZ: errors.append(W001) return errors + + +@register(deploy=True) +def check_rsa_private_key_exists(app_configs, **kwargs): + """ + Ensure an RSA private key exists to enable OIDC. + """ + msgs = [] + if generate_rsa_private_key(settings.BASE_DIR): + msgs.append(Info("An RSA private key has been generated for OIDC.")) + return msgs diff --git a/modoboa/core/checks/__init__.py b/modoboa/core/checks/__init__.py deleted file mode 100644 index 9b716b17c..000000000 --- a/modoboa/core/checks/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Import these to force registration of checks -from . import settings_checks # NOQA:F401 diff --git a/modoboa/core/commands/deploy.py b/modoboa/core/commands/deploy.py index 76a2fd7b7..16954c381 100644 --- a/modoboa/core/commands/deploy.py +++ b/modoboa/core/commands/deploy.py @@ -9,8 +9,6 @@ import string import subprocess import sys -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa import dj_database_url import django @@ -19,6 +17,7 @@ from django.template import Context, Template from django.utils.encoding import smart_str from modoboa.core.commands import Command +from modoboa.core.utils import generate_rsa_private_key from modoboa.lib.api_client import ModoAPIClient from modoboa.lib.sysutils import exec_cmd @@ -199,22 +198,6 @@ class DeployCommand(Command): extra_settings.append(extension[1]) return extra_settings - def generate_rsa_private_key(self, storage_path: str): - """Generate RSA private key for OIDC support.""" - private_key = rsa.generate_private_key( - public_exponent=65537, - key_size=4096, - ) - pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - content = bytes(pem) - content = content.replace(b"\n", b"\\n") - with open(f"{storage_path}/.env", "wb") as fp: - fp.write(b'OIDC_RSA_PRIVATE_KEY="' + content + b'"\n') - def handle(self, parsed_args): django.setup() management.call_command("startproject", parsed_args.name, verbosity=False) @@ -285,7 +268,7 @@ class DeployCommand(Command): ) with open("%s/settings.py" % path, "w") as fp: fp.write(tpl) - self.generate_rsa_private_key(parsed_args.name) + generate_rsa_private_key(parsed_args.name) shutil.copyfile("%s/urls.py.tpl" % self._templates_dir, "%s/urls.py" % path) os.mkdir("%s/media" % parsed_args.name) diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index ae51e60b3..99874903f 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -112,12 +112,17 @@ class Command(BaseCommand): allowed_host = input("What will be the hostname used to access Modoboa? ") if not allowed_host: allowed_host = "localhost" - frontend_application = app_model.objects.filter(name="Modoboa frontend") + frontend_application_qs = app_model.objects.all() + frontend_application = None + for app in frontend_application_qs: + if app.name == "Modoboa frontend": + frontend_application = app + break frontend_path = getattr(settings, "NEW_ADMIN_URL", "new-admin") base_uri = f"https://{allowed_host}/{frontend_path}" redirect_uri = f"{base_uri}/login/logged" client_id = "" - if not frontend_application.exists(): + if frontend_application is None: if options["dev"]: base_uri = "https://localhost:3000/" redirect_uri = "https://localhost:3000/login/logged" @@ -136,13 +141,14 @@ class Command(BaseCommand): "authorization-code", ) else: - client_id = frontend_application.first().client_id + client_id = frontend_application.client_id base_frontend_dir = os.path.join( - os.path.dirname(__file__), "../../frontend_dist/" + os.path.dirname(__file__), "../../../frontend_dist/" ) - frontend_target_dir = "{}/frontend".format(options["name"]) + frontend_target_dir = "{}/frontend".format(settings.BASE_DIR) if os.path.exists(base_frontend_dir): + shutil.rmtree(frontend_target_dir, ignore_errors=True) shutil.copytree(base_frontend_dir, frontend_target_dir) with open(f"{frontend_target_dir}/config.json", "w") as fp: fp.write( @@ -150,7 +156,7 @@ class Command(BaseCommand): "API_BASE_URL": "https://{allowed_host}/api/v2", "OAUTH_AUTHORITY_URL": "https://{allowed_host}/api/o", "OAUTH_CLIENT_ID": "{client_id}", - "OAUTH_REDIRECT_URI": "{redirect_uri}" + "OAUTH_REDIRECT_URI": "{redirect_uri}", "OAUTH_POST_REDIRECT_URI": "{base_uri}" }} """ diff --git a/modoboa/core/tests/test_checks.py b/modoboa/core/tests/test_checks.py index 02bdfeb60..b3beda5dd 100644 --- a/modoboa/core/tests/test_checks.py +++ b/modoboa/core/tests/test_checks.py @@ -1,18 +1,37 @@ +import os +import shutil +import tempfile + from django.test import SimpleTestCase from django.test.utils import override_settings -from ..checks import settings_checks +from modoboa.core import checks class CheckSessionCookieSecureTest(SimpleTestCase): + + def setUp(self): + self.workdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.workdir) + @override_settings(USE_TZ=False) def test_use_tz_false(self): """If USE_TZ is off provide one warning.""" - self.assertEqual( - settings_checks.check_use_tz_enabled(None), [settings_checks.W001] - ) + self.assertEqual(checks.check_use_tz_enabled(None), [checks.W001]) @override_settings(USE_TZ=True) def test_use_tz_true(self): """If USE_TZ is on, there's no warning about it.""" - self.assertEqual(settings_checks.check_use_tz_enabled(None), []) + self.assertEqual(checks.check_use_tz_enabled(None), []) + + def test_rsa_private_key_exists(self): + with override_settings(BASE_DIR=self.workdir): + os.environ["OIDC_RSA_PRIVATE_KEY"] = "NONE" + msgs = checks.check_rsa_private_key_exists(None) + self.assertEqual(len(msgs), 1) + self.assertTrue(os.path.exists(f"{self.workdir}/.env")) + os.environ["OIDC_RSA_PRIVATE_KEY"] = "KEY" + msgs = checks.check_rsa_private_key_exists(None) + self.assertEqual(len(msgs), 0) diff --git a/modoboa/core/utils.py b/modoboa/core/utils.py index 1ef002d64..f096b3d63 100644 --- a/modoboa/core/utils.py +++ b/modoboa/core/utils.py @@ -1,12 +1,16 @@ """Utility functions.""" from pkg_resources import parse_version +import os +import environ + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa from django.utils.translation import gettext as _ from modoboa.core.extensions import exts_pool from modoboa.lib.api_client import ModoAPIClient -from . import models def parse_map_file(path): @@ -23,6 +27,8 @@ def parse_map_file(path): def check_for_updates(): """Check if a new version of Modoboa is available.""" + from . import models + local_config = models.LocalConfig.objects.first() client = ModoAPIClient() extensions = exts_pool.list_all() @@ -49,3 +55,33 @@ def check_for_updates(): update_avail = True break return update_avail, extensions + + +def generate_rsa_private_key(storage_path: str) -> bool: + """Generate RSA private key for OIDC support. + + :return: False if the key was not generated + and True if it was.""" + + env_path = os.path.join(storage_path, ".env") + + env = environ.Env(OIDC_RSA_PRIVATE_KEY=(str, "NONE")) + environ.Env.read_env(env_path) + + if env("OIDC_RSA_PRIVATE_KEY") != "NONE": + return False + + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=4096, + ) + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + content = bytes(pem) + content = content.replace(b"\n", b"\\n") + with open(env_path, "wb") as fp: + fp.write(b'OIDC_RSA_PRIVATE_KEY="' + content + b'"\n') + return True