From b7dc1949361905690e7c1dbb9c5a3f6ac9c2f15b Mon Sep 17 00:00:00 2001
From: Benjamin Renard <brenard@easter-eggs.com>
Date: Thu, 10 Nov 2022 15:08:33 +0100
Subject: [PATCH] PasswordResetConfirmView: fix handling PasswordChangeError

---
 src/authentic2/views.py      |  6 +++-
 tests/test_ldap.py           | 54 ++++++++++++++++++++++++++++++++++++
 tests/test_password_reset.py | 21 ++++++++++++++
 3 files changed, 80 insertions(+), 1 deletion(-)

diff --git a/src/authentic2/views.py b/src/authentic2/views.py
index 69e58edc..8a474e16 100644
--- a/src/authentic2/views.py
+++ b/src/authentic2/views.py
@@ -994,7 +994,11 @@ class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView):
     def form_valid(self, form):
         # Changing password by mail validate the email
         form.user.set_email_verified(True)
-        form.save()
+        try:
+            form.save()
+        except utils_misc.PasswordChangeError as e:
+            form.add_error('new_password1', e.message)
+            return self.form_invalid(form)
         hooks.call_hooks('event', name='password-reset-confirm', user=form.user, token=self.token, form=form)
         logger.info('password reset for user %s with token %r', self.user, self.token.uuid)
         self.token.delete()
diff --git a/tests/test_ldap.py b/tests/test_ldap.py
index ff081c9a..8f4eed62 100644
--- a/tests/test_ldap.py
+++ b/tests/test_ldap.py
@@ -1102,6 +1102,60 @@ def test_reset_password_ldap_failure(slapd, settings, app, db, caplog):
     assert 'account is from ldap but it could not be retrieved' in caplog.text
 
 
+def test_reset_password_refused_by_ldap_server(slapd, settings, app, db, caplog):
+    settings.LDAP_AUTH_SETTINGS = [
+        {
+            'url': [slapd.ldap_url],
+            'binddn': force_str(slapd.root_bind_dn),
+            'bindpw': force_str(slapd.root_bind_password),
+            'basedn': 'o=ôrga',
+            'use_tls': False,
+            'attributes': ['uid', 'carLicense'],
+            'can_reset_password': True,
+        }
+    ]
+
+    assert User.objects.count() == 0
+    # first login
+    response = app.get('/login/')
+    response.form['username'] = USERNAME
+    response.form['password'] = PASS
+    response = response.form.submit('login-password-submit').follow()
+    assert User.objects.count() == 1
+    assert 'Étienne Michu' in str(response)
+    user = User.objects.get()
+    assert user.email == EMAIL
+    # logout
+    response = response.click('Logout').maybe_follow()
+
+    # password reset
+    response = response.click('Reset it!')
+    response.form['email'] = EMAIL
+    assert len(mail.outbox) == 0
+    response = response.form.submit()
+    assert response['Location'].endswith('/instructions/')
+    assert len(mail.outbox) == 1
+    url = utils.get_link_from_mail(mail.outbox[0])
+    relative_url = url.split('testserver')[1]
+    response = app.get(relative_url, status=200)
+    response.form.set('new_password1', '1234==aA')
+    response.form.set('new_password2', '1234==aA')
+
+    # Make LDAP directory as read-only to trigger an error
+    conn = slapd.get_connection_admin()
+    ldif = [
+        (
+            ldap.MOD_REPLACE,
+            'olcReadOnly',
+            b'TRUE',
+        )
+    ]
+    conn.modify_s('olcDatabase={%s}mdb,cn=config' % (slapd.db_index - 1), ldif)
+
+    response = response.form.submit()
+    assert 'LDAP directory refused the password change' in response
+
+
 def test_user_cannot_change_password(slapd, settings, app, db):
     settings.LDAP_AUTH_SETTINGS = [
         {
diff --git a/tests/test_password_reset.py b/tests/test_password_reset.py
index e0646616..fe4e401b 100644
--- a/tests/test_password_reset.py
+++ b/tests/test_password_reset.py
@@ -68,6 +68,27 @@ def test_reset_by_email(app, simple_user, mailoutbox, settings):
         app.get(url, status=404)
 
 
+def test_reset_by_email_passwords_not_match(app, simple_user, mailoutbox, settings):
+    url = reverse('password_reset')
+    resp = app.get(url, status=200)
+    resp.form.set('email', simple_user.email)
+    assert len(mailoutbox) == 0
+    settings.DEFAULT_FROM_EMAIL = 'show only addr <noreply@example.net>'
+    resp = resp.form.submit()
+    utils.assert_event('user.password.reset.request', user=simple_user, email=simple_user.email)
+    assert resp['Location'].endswith('/instructions/')
+    resp = resp.follow()
+    assert len(mailoutbox) == 1
+    url = utils.get_link_from_mail(mailoutbox[0])
+    relative_url = url.split('testserver')[1]
+    resp = app.get(relative_url, status=200)
+    resp.form.set('new_password1', '1234==aA')
+    resp.form.set('new_password2', '1234')
+    resp = resp.form.submit()
+
+    assert 'Passwords do not match.' in resp
+
+
 def test_can_reset_by_username(app, db, simple_user, settings, mailoutbox):
     resp = app.get('/password/reset/')
     assert 'email_or_username' not in resp.form.fields
-- 
2.30.2

