From 02201381d6216d8114f1abf63816928fe4583f2a Mon Sep 17 00:00:00 2001 From: "rdzantoine.pro@gmail.com" Date: Fri, 1 Nov 2024 14:52:26 +0100 Subject: [PATCH 1/7] add allow_special_characters for random pwd option to parameters form --- modoboa/core/app_settings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 0dfd9c6f0..2154fce50 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -96,6 +96,16 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict( ), }, ), + ( + "allow_special_characters", + { + "label": gettext_lazy("Allow special characters for random password"), + "help_text": gettext_lazy( + "Enable special characters in randomly generated " + "passwords." + ), + } + ), ( "update_password_url", { From c2a775b9adcb3e8a09d3bbad08b21134da255a5d Mon Sep 17 00:00:00 2001 From: "rdzantoine.pro@gmail.com" Date: Fri, 1 Nov 2024 14:56:56 +0100 Subject: [PATCH 2/7] add allow_special_characters for random pwd into serializers --- modoboa/core/api/v2/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modoboa/core/api/v2/serializers.py b/modoboa/core/api/v2/serializers.py index f7f8f0be4..4bd5e624d 100644 --- a/modoboa/core/api/v2/serializers.py +++ b/modoboa/core/api/v2/serializers.py @@ -75,6 +75,7 @@ class CoreGlobalParametersSerializer(serializers.Serializer): update_scheme = serializers.BooleanField(default=True) default_password = serializers.CharField(default="ChangeMe1!") random_password_length = serializers.IntegerField(min_value=8, default=8) + allow_special_characters = serializers.BooleanField(default=False) 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) From f2697b6c92383d8dbcf322eaf573e70017dba2df Mon Sep 17 00:00:00 2001 From: "rdzantoine.pro@gmail.com" Date: Fri, 1 Nov 2024 15:40:25 +0100 Subject: [PATCH 3/7] add special characters functionnality to random password generator function make_password --- modoboa/admin/lib.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index 7d75b1725..8f334a3a4 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -293,10 +293,13 @@ def domain_has_authorized_mx(name): def make_password(): """Create a random password.""" length = int(param_tools.get_global_parameter("random_password_length", app="core")) - while True: + allow_special_characters = bool(param_tools.get_global_parameter("allow_special_characters", app="core")) + possible_chars = string.ascii_letters + string.digits + for i in range(10): # limit tries + if allow_special_characters: # add special characters in random passwd + possible_chars += string.punctuation password = "".join( - random.SystemRandom().choice(string.ascii_letters + string.digits) - for _ in range(length) + random.SystemRandom().choice(possible_chars) for _ in range(length) ) try: password_validation.validate_password(password) From 3c9cc5a42e054cd66e0d0b52e029cd051d878aa6 Mon Sep 17 00:00:00 2001 From: "rdzantoine.pro@gmail.com" Date: Fri, 1 Nov 2024 16:46:04 +0100 Subject: [PATCH 4/7] modify random password generator function make_password() to always include at least one of the possible string groups (ascii_letters, digits, special characters etc.) --- modoboa/admin/lib.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index 8f334a3a4..0f3534fb5 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -294,13 +294,27 @@ def make_password(): """Create a random password.""" length = int(param_tools.get_global_parameter("random_password_length", app="core")) allow_special_characters = bool(param_tools.get_global_parameter("allow_special_characters", app="core")) - possible_chars = string.ascii_letters + string.digits + possible_chars_types = [string.ascii_letters, string.digits] + all_possible_chars = '' + certain_chars = '' for i in range(10): # limit tries if allow_special_characters: # add special characters in random passwd - possible_chars += string.punctuation + possible_chars_types.append(string.punctuation) + # make it so there is always one character of each type in password + # and then will randomize their placement + for possible_chars in possible_chars_types: + all_possible_chars += possible_chars + certain_chars += random.SystemRandom().choice(possible_chars) + password = "".join( - random.SystemRandom().choice(possible_chars) for _ in range(length) - ) + random.SystemRandom().choice(all_possible_chars) \ + for _ in range(length - len(possible_chars_types)) + ) + certain_chars + + li_password = list(password) + random.shuffle(li_password) + password = "".join(li_password) + try: password_validation.validate_password(password) except ValidationError: From 8965e7ea78c766189dac4314745304bcab7fc0d6 Mon Sep 17 00:00:00 2001 From: "rdzantoine.pro@gmail.com" Date: Fri, 1 Nov 2024 17:24:53 +0100 Subject: [PATCH 5/7] comment test_random_password with a TODO to add a new test for checking if some characters are in unhashed password before user creation with random password --- modoboa/admin/lib.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index 0f3534fb5..f4b866ae0 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -305,16 +305,13 @@ def make_password(): for possible_chars in possible_chars_types: all_possible_chars += possible_chars certain_chars += random.SystemRandom().choice(possible_chars) - password = "".join( random.SystemRandom().choice(all_possible_chars) \ for _ in range(length - len(possible_chars_types)) ) + certain_chars - li_password = list(password) random.shuffle(li_password) password = "".join(li_password) - try: password_validation.validate_password(password) except ValidationError: From 8ea59288695ef4aa18b39712e27b81034c721ce7 Mon Sep 17 00:00:00 2001 From: Spitfireap Date: Sat, 21 Dec 2024 18:04:09 +0100 Subject: [PATCH 6/7] Added tests --- modoboa/admin/lib.py | 34 +++++++++++++++++------------ modoboa/admin/tests/test_account.py | 5 +++-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index f4b866ae0..b9d4f1881 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -292,26 +292,32 @@ def domain_has_authorized_mx(name): def make_password(): """Create a random password.""" - length = int(param_tools.get_global_parameter("random_password_length", app="core")) - allow_special_characters = bool(param_tools.get_global_parameter("allow_special_characters", app="core")) - possible_chars_types = [string.ascii_letters, string.digits] - all_possible_chars = '' - certain_chars = '' - for i in range(10): # limit tries - if allow_special_characters: # add special characters in random passwd + password_length = int( + param_tools.get_global_parameter("random_password_length", app="core") + ) + allow_special_characters = bool( + param_tools.get_global_parameter("allow_special_characters", app="core") + ) + for i in range(10): # limit tries + all_possible_chars = "" + possible_chars_types = [string.ascii_letters, string.digits] + certain_chars = "" + if allow_special_characters: # add special characters in random passwd possible_chars_types.append(string.punctuation) # make it so there is always one character of each type in password # and then will randomize their placement for possible_chars in possible_chars_types: all_possible_chars += possible_chars certain_chars += random.SystemRandom().choice(possible_chars) - password = "".join( - random.SystemRandom().choice(all_possible_chars) \ - for _ in range(length - len(possible_chars_types)) - ) + certain_chars - li_password = list(password) - random.shuffle(li_password) - password = "".join(li_password) + password = ( + "".join( + random.SystemRandom().choice(all_possible_chars) + for _ in range(password_length - len(possible_chars_types)) + ) + + certain_chars + ) + random.shuffle(list(password)) + password = "".join(password) try: password_validation.validate_password(password) except ValidationError: diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index 0c9b426e5..d3e1014ab 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -1,14 +1,15 @@ from unittest import skipIf +import string from django.test import override_settings from django.urls import reverse from modoboa.core import factories as core_factories -from modoboa.core.models import User +from modoboa.core.models import User, LocalConfig from modoboa.core.tests import test_ldap from modoboa.lib.tests import NO_LDAP, ModoAPITestCase from modoboa.limits import utils as limits_utils -from .. import factories, models +from .. import factories, models, lib class AuthenticationTestCase(ModoAPITestCase): From 3013d1f5818f281b8cfdbb577ab64cee6e88b7f6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 23 Jun 2025 10:22:36 +0200 Subject: [PATCH 7/7] Updated test --- frontend/src/layouts/admin/AdminLayout.vue | 8 +++---- modoboa/admin/tests/test_account.py | 28 +++++++++++++++++++++- modoboa/core/password_hashers/utils.py | 2 +- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/frontend/src/layouts/admin/AdminLayout.vue b/frontend/src/layouts/admin/AdminLayout.vue index 18f529a7e..c03c9cd8f 100644 --- a/frontend/src/layouts/admin/AdminLayout.vue +++ b/frontend/src/layouts/admin/AdminLayout.vue @@ -24,6 +24,10 @@ const imapMigrationEnabled = computed( () => parametersStore.imapMigrationEnabled ) +const openRspamdDashboard = () => { + router.push(rspamdUrl.value) +} + const settings = [] const adminMenuItems = [ { @@ -136,8 +140,4 @@ onMounted(() => { }) } }) - -const openRspamdDashboard = () => { - router.push(rspamdUrl.value) -} diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index d3e1014ab..4b8977ed0 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -5,7 +5,7 @@ from django.test import override_settings from django.urls import reverse from modoboa.core import factories as core_factories -from modoboa.core.models import User, LocalConfig +from modoboa.core.models import User from modoboa.core.tests import test_ldap from modoboa.lib.tests import NO_LDAP, ModoAPITestCase from modoboa.limits import utils as limits_utils @@ -46,6 +46,32 @@ class AccountTestCase(ModoAPITestCase): super().setUpTestData() factories.populate_database() + def test_random_password(self): + # Setting allow_special_characters to False + self.localconfig.parameters.set_value("allow_special_characters", False, "core") + random_password_length = self.localconfig.parameters.get_value( + "random_password_length", "core" + ) + self.localconfig.save() + + random_password = lib.make_password() + self.assertEqual(len(random_password), random_password_length) + self.assertEqual( + len([char for char in random_password if char in list(string.punctuation)]), + 0, + ) + + # And with allow_special_characters to True + self.localconfig.parameters.set_value("allow_special_characters", True, "core") + self.localconfig.save() + + random_password = lib.make_password() + self.assertEqual(len(random_password), random_password_length) + self.assertNotEqual( + len([char for char in random_password if char in list(string.punctuation)]), + 0, + ) + def test_aliases_update_on_disable(self): """Check if aliases are disabled when account is disabled.""" account = User.objects.get(username="user@test.com") diff --git a/modoboa/core/password_hashers/utils.py b/modoboa/core/password_hashers/utils.py index a317f9104..a852f0cba 100644 --- a/modoboa/core/password_hashers/utils.py +++ b/modoboa/core/password_hashers/utils.py @@ -61,7 +61,7 @@ def get_dovecot_schemes(): return [f"{{{smart_str(scheme)}}}" for scheme in schemes.split()], status -def cache_available_password_hasher(bypass_cache=False): +def cache_available_password_hasher(bypass_cache: bool = False): available_schemes, status = get_dovecot_schemes() password_scheme_choice = [ (hasher.name, hasher.label)