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/lib.py b/modoboa/admin/lib.py index 7d75b1725..b9d4f1881 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -292,12 +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")) - while True: - password = "".join( - random.SystemRandom().choice(string.ascii_letters + string.digits) - for _ in range(length) + 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(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..4b8977ed0 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -1,4 +1,5 @@ from unittest import skipIf +import string from django.test import override_settings from django.urls import reverse @@ -8,7 +9,7 @@ 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 -from .. import factories, models +from .. import factories, models, lib class AuthenticationTestCase(ModoAPITestCase): @@ -45,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/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) 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", { 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)