Repository: modoboa/modoboa

Issue:
Wishlist:  Convert old password scheme at login
Don't know if this already exist.

When login, modoboa will check password scheme and convert it to the default scheme if needed.




Discussion:

Resolving patch:
diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py
--- a/modoboa/core/app_settings.py
+++ b/modoboa/core/app_settings.py
@@ -68,6 +68,14 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
         widget=forms.TextInput(attrs={"class": "form-control"})
     )
 
+    update_scheme = YesNoField(
+        label=ugettext_lazy("Update password scheme at login"),
+        initial=True,
+        help_text=ugettext_lazy(
+            "Update user password at login to use the default password scheme"
+        )
+    )
+
     default_password = forms.CharField(
         label=ugettext_lazy("Default password"),
         initial="password",
diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py
--- a/modoboa/core/views/auth.py
+++ b/modoboa/core/views/auth.py
@@ -17,6 +17,8 @@
 from django.views.decorators.cache import never_cache
 
 from modoboa.core import forms
+from modoboa.core.password_hashers import get_password_hasher
+from modoboa.parameters import tools as param_tools
 from .. import signals
 from .base import find_nextlocation
 
@@ -33,6 +35,24 @@ def dologin(request):
             user = authenticate(username=form.cleaned_data["username"],
                                 password=form.cleaned_data["password"])
             if user and user.is_active:
+                if param_tools.get_global_parameter("update_scheme",
+                                                    raise_exception=False):
+                    # check if password scheme is correct
+                    scheme = param_tools.get_global_parameter(
+                        "password_scheme", raise_exception=False)
+                    # use SHA512CRYPT as default fallback
+                    if scheme is None:
+                        pwhash = get_password_hasher('sha512crypt')()
+                    else:
+                        pwhash = get_password_hasher(scheme)()
+                    if not user.password.startswith(pwhash.scheme):
+                        logging.info(
+                            _("Password scheme mismatch. Updating %s password"),
+                            user.username
+                        )
+                        user.set_password(form.cleaned_data["password"])
+                        user.save()
+
                 login(request, user)
                 if not form.cleaned_data["rememberme"]:
                     request.session.set_expiry(0)


Test patch:
diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py
--- a/modoboa/core/tests/test_authentication.py
+++ b/modoboa/core/tests/test_authentication.py
@@ -11,6 +11,7 @@
 from django.test import override_settings
 from django.urls import reverse
 
+from modoboa.core.password_hashers import get_password_hasher
 from modoboa.lib.tests import NO_SMTP, ModoTestCase
 from .. import factories, models
 
@@ -67,6 +68,39 @@ def test_open_redirect(self):
         self.assertEqual(response.status_code, 302)
         self.assertTrue(response.url.endswith(reverse("core:dashboard")))
 
+    def test_password_scheme(self):
+        """Validate password scheme changes."""
+        username = "user@test.com"
+        password = "toto"
+        data = {"username": username, "password": password}
+        user = models.User.objects.get(username=username)
+        pw_hash = get_password_hasher('fallback_scheme')()
+
+        self.client.logout()
+        self.set_global_parameter("password_scheme", "sha512crypt")
+        self.client.post(reverse("core:login"), data)
+        user.refresh_from_db()
+        self.assertTrue(user.password.startswith("{SHA512-CRYPT}"))
+
+        self.client.logout()
+        self.set_global_parameter("password_scheme", "sha256")
+        self.client.post(reverse("core:login"), data)
+        user.refresh_from_db()
+        self.assertTrue(user.password.startswith("{SHA256}"))
+
+        self.client.logout()
+        self.set_global_parameter("password_scheme", "fallback_scheme")
+        self.client.post(reverse("core:login"), data)
+        user.refresh_from_db()
+        self.assertTrue(user.password.startswith(pw_hash.scheme))
+
+        self.client.logout()
+        self.set_global_parameter("password_scheme", "sha256crypt")
+        self.set_global_parameter("update_scheme", False)
+        self.client.post(reverse("core:login"), data)
+        user.refresh_from_db()
+        self.assertTrue(user.password.startswith(pw_hash.scheme))
+
 
 class PasswordResetTestCase(ModoTestCase):
     """Test password reset service."""
diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py
--- a/modoboa/core/tests/test_views.py
+++ b/modoboa/core/tests/test_views.py
@@ -36,6 +36,7 @@
     "csrfmiddlewaretoken": "SGgMVZsA4TPqoiV786TMST6xgOlhAf4F",
     "limits-deflt_user_mailboxes_limit": "0",
     "core-password_scheme": "sha512crypt",
+    "core-update_scheme": True,
     "core-items_per_page": "30",
     "limits-deflt_user_mailbox_aliases_limit": "0",
     "limits-deflt_domain_mailboxes_limit": "0",