From bd0b71fe28a89f23b243e36b16e0be397e5bb218 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Tue, 27 Oct 2020 12:18:37 +0100
Subject: [PATCH 2/2] a2_rbac: fix inconsistencies in OUs' user password reset
 option (#46650)

---
 .../migrations/0025_auto_20210622_1132.py     | 21 ++++++++++
 src/authentic2/a2_rbac/models.py              | 10 ++++-
 src/authentic2/views.py                       |  6 ++-
 tests/test_password_reset.py                  | 38 +++++++++++++++++++
 4 files changed, 73 insertions(+), 2 deletions(-)
 create mode 100644 src/authentic2/a2_rbac/migrations/0025_auto_20210622_1132.py

diff --git a/src/authentic2/a2_rbac/migrations/0025_auto_20210622_1132.py b/src/authentic2/a2_rbac/migrations/0025_auto_20210622_1132.py
new file mode 100644
index 000000000..f3b4d8d00
--- /dev/null
+++ b/src/authentic2/a2_rbac/migrations/0025_auto_20210622_1132.py
@@ -0,0 +1,21 @@
+# Generated by Django 2.2.19 on 2021-06-22 09:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('a2_rbac', '0024_fix_self_admin_perm'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='organizationalunit',
+            name='user_can_reset_password',
+            field=models.NullBooleanField(
+                choices=[(None, 'System default'), (True, 'Yes'), (False, 'No')],
+                verbose_name='Users can reset password',
+            ),
+        ),
+    ]
diff --git a/src/authentic2/a2_rbac/models.py b/src/authentic2/a2_rbac/models.py
index 068db9dcc..2508b76c0 100644
--- a/src/authentic2/a2_rbac/models.py
+++ b/src/authentic2/a2_rbac/models.py
@@ -56,6 +56,12 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase):
         (MANUAL_PASSWORD_POLICY, _('Manual password definition')),
     )
 
+    USER_CAN_RESET_PASSWD_CHOICES = (
+        (None, _("System default")),
+        (True, _("Yes")),
+        (False, _("No")),
+    )
+
     PolicyValue = namedtuple(
         'PolicyValue',
         ['generate_password', 'reset_password_at_next_login', 'send_mail', 'send_password_reset'],
@@ -78,7 +84,9 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase):
         rbac_utils.get_permission_model_name(), content_type_field='target_ct', object_id_field='target_id'
     )
 
-    user_can_reset_password = models.NullBooleanField(verbose_name=_('Users can reset password'))
+    user_can_reset_password = models.NullBooleanField(
+        verbose_name=_('Users can reset password'), choices=USER_CAN_RESET_PASSWD_CHOICES
+    )
 
     user_add_password_policy = models.IntegerField(
         verbose_name=_('User creation password policy'), choices=USER_ADD_PASSWD_POLICY_CHOICES, default=0
diff --git a/src/authentic2/views.py b/src/authentic2/views.py
index dd11a0d2d..5c6f4057c 100644
--- a/src/authentic2/views.py
+++ b/src/authentic2/views.py
@@ -794,7 +794,11 @@ class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView):
         can_reset_password = utils.get_user_flag(
             user=self.user, name='can_reset_password', default=self.user.has_usable_password()
         )
-        if not can_reset_password:
+        if (
+            can_reset_password is False
+            or can_reset_password is None
+            and not app_settings.A2_USER_CAN_RESET_PASSWORD
+        ):
             messages.warning(
                 request, _('It\'s not possible to reset your password. Please contact an administrator.')
             )
diff --git a/tests/test_password_reset.py b/tests/test_password_reset.py
index 6a8149c69..ed7ae9599 100644
--- a/tests/test_password_reset.py
+++ b/tests/test_password_reset.py
@@ -13,6 +13,8 @@
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
+import re
+
 import pytest
 from django.test.utils import override_settings
 from django.urls import reverse
@@ -207,3 +209,39 @@ def test_honeypot(app, db, settings, mailoutbox):
     response = response.follow()
     assert len(mailoutbox) == 0
     assert 'Your password reset request has been refused' in response
+
+
+def test_ou_policies(app, db, settings, user_ou1, ou1, user_ou2, ou2, mailoutbox):
+
+    settings.A2_USER_CAN_RESET_PASSWORD = True
+
+    user_ou1.email = 'john.doe.ou1@example.net'
+    user_ou1.save()
+    ou1.user_can_reset_password = False  # impossible
+    ou1.save()
+
+    url = reverse('password_reset')
+    resp = app.get(url, status=200)
+    resp.form.set('email', user_ou1.email)
+    resp = resp.form.submit()
+    url = utils.get_link_from_mail(mailoutbox[0])
+    relative_url = url.split('testserver')[1]
+    resp = app.get(relative_url, status=302)  # impossible, redirected to /
+    assert resp['Location'] == '/'
+
+    ou2.user_can_reset_password = None  # system default
+    ou2.save()
+
+    url = reverse('password_reset')
+    resp = app.get(url, status=200)
+    resp.form.set('email', user_ou2.email)
+    resp = resp.form.submit()
+    url = utils.get_link_from_mail(mailoutbox[1])
+    relative_url = url.split('testserver')[1]
+    resp = app.get(relative_url, status=200)
+    assert 'In order to create a secure password' in resp.text
+
+    settings.A2_USER_CAN_RESET_PASSWORD = False
+
+    url = reverse('password_reset')
+    resp = app.get(url, status=404)  # globally deactivated, page not found
-- 
2.32.0

