From 1c012fe7f30c24ed1677684d591614bc1e2240e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Dachary?= <ldachary@easter-eggs.com>
Date: Fri, 19 Feb 2021 13:49:22 +0100
Subject: [PATCH] ldap: move messages from hard coded to settings (#51294)

Fixes: #51294

Licenses: MIT
---
 src/authentic2/backends/ldap_backend.py | 63 ++++++++++++-------------
 tests/test_ldap.py                      |  6 +--
 2 files changed, 34 insertions(+), 35 deletions(-)

diff --git a/src/authentic2/backends/ldap_backend.py b/src/authentic2/backends/ldap_backend.py
index 676f082e9..63a435203 100644
--- a/src/authentic2/backends/ldap_backend.py
+++ b/src/authentic2/backends/ldap_backend.py
@@ -45,9 +45,9 @@ from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group
 from django.core.cache import cache
 from django.core.exceptions import ImproperlyConfigured
+from django.template import loader
 from django.utils.encoding import force_bytes, force_text
-from django.utils.translation import ngettext
-from django.utils.translation import ugettext as _
+from django.utils.translation import ugettext as _, gettext, ngettext
 
 from authentic2 import app_settings, crypto
 from authentic2.a2_rbac.models import Role
@@ -299,39 +299,23 @@ def map_text(d):
     raise NotImplementedError
 
 
-def password_policy_control_messages(ctrl):
-    messages = []
+def password_policy_control_messages(ctrl, control_messages):
+    results = []
 
     if ctrl.error:
         error = ppolicy.PasswordPolicyError.namedValues[ctrl.error]
-        error2message = {
-            'passwordExpired': _('The password expired'),
-            'accountLocked': _('The account is locked.'),
-            'changeAfterReset': _('The password was reset and must be changed.'),
-            'passwordModNotAllowed': _('It is not possible to modify the password.'),
-            'mustSupplyOldPassword': _('The old password must be supplied.'),
-            'insufficientPasswordQuality': _('The password does not meet the quality requirements.'),
-            'passwordTooShort': _('The password is too short.'),
-            'passwordTooYoung': _('It is too soon to change the password.'),
-            'passwordInHistory': _('This password was recently used and cannot be used again.'),
-        }
-        messages.append(error2message.get(error, _('Unexpected error {error}').format(error=error)))
-        return messages
+        if error not in control_messages:
+            results.append(_('Unexpected error {error}').format(error=error))
+        else:
+            results.append(control_messages[error])
+        return results
 
     if ctrl.timeBeforeExpiration:
         expiration_date = time.asctime(time.localtime(time.time() + ctrl.timeBeforeExpiration))
-        messages.append(
-            _('The password will expire at {expiration_date}.').format(expiration_date=expiration_date)
-        )
+        results.append(control_messages['expiration_date'].format(expiration_date=expiration_date))
     if ctrl.graceAuthNsRemaining:
-        messages.append(
-            ngettext(
-                'This password expired: this is the last time it can be used.',
-                'This password expired and can only be used {graceAuthNsRemaining} times, including this one.',
-                ctrl.graceAuthNsRemaining,
-            ).format(graceAuthNsRemaining=ctrl.graceAuthNsRemaining)
-        )
-    return messages
+        results.append(control_messages['graceAuthNsRemaining'].format(graceAuthNsRemaining=ctrl.graceAuthNsRemaining))
+    return results
 
 
 class LDAPUser(User):
@@ -621,6 +605,21 @@ class LDAPBackend(object):
         'user_attributes': [],
         # https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#ldap-controls
         'use_controls': True,
+        'ppolicy_messages': {
+            'passwordExpired': gettext('The password expired'),
+            'accountLocked': gettext('The account is locked.'),
+            'changeAfterReset': gettext('The password was reset and must be changed.'),
+            'passwordModNotAllowed': gettext('It is not possible to modify the password.'),
+            'mustSupplyOldPassword': gettext('The old password must be supplied.'),
+            'insufficientPasswordQuality': gettext('The password does not meet the quality requirements.'),
+            'passwordTooShort': gettext('The password is too short.'),
+            'passwordTooYoung': gettext('It is too soon to change the password.'),
+            'passwordInHistory': gettext('This password was recently used and cannot be used again.'),
+            'expiration_date': gettext('The password will expire at {expiration_date}.'),
+            'graceAuthNsRemaining': gettext(
+                'This password expired and can only be used {graceAuthNsRemaining} time(s), including this one.'
+            ),
+        },
     }
     _REQUIRED = ('url', 'basedn')
     _TO_ITERABLE = ('url', 'groupsu', 'groupstaff', 'groupactive')
@@ -660,10 +659,10 @@ class LDAPBackend(object):
         return blocks
 
     @staticmethod
-    def process_controls(request, authz_id, ctrls):
+    def process_controls(request, authz_id, ctrls, block):
         for c in ctrls:
             if c.controlType == ppolicy.PasswordPolicyControl.controlType:
-                message = ' '.join(password_policy_control_messages(c))
+                message = ' '.join(password_policy_control_messages(c, block['ppolicy_messages']))
                 if request is not None:
                     messages.add_message(request, messages.WARNING, message)
                     if c.graceAuthNsRemaining or c.timeBeforeExpiration:
@@ -791,7 +790,7 @@ class LDAPBackend(object):
                             else:
                                 serverctrls = []
                             results = conn.simple_bind_s(authz_id, password, serverctrls=serverctrls)
-                            self.process_controls(request, authz_id, results[3])
+                            self.process_controls(request, authz_id, results[3], block)
                             user_login_success(authz_id)
                             if not block['connect_with_user_credentials']:
                                 try:
@@ -803,7 +802,7 @@ class LDAPBackend(object):
                         except ldap.INVALID_CREDENTIALS as e:
                             if block.get('use_controls') and len(e.args) > 0 and 'ctrls' in e.args[0]:
                                 self.process_controls(
-                                    request, authz_id, DecodeControlTuples(e.args[0]['ctrls'])
+                                    request, authz_id, DecodeControlTuples(e.args[0]['ctrls']), block
                                 )
                             attributes = self.get_ldap_attributes(block, conn, authz_id)
                             user = self.lookup_existing_user(authz_id, block, attributes)
diff --git a/tests/test_ldap.py b/tests/test_ldap.py
index 615c3d25d..a049fd1ee 100644
--- a/tests/test_ldap.py
+++ b/tests/test_ldap.py
@@ -1229,11 +1229,11 @@ pwdSafeModify: FALSE
 
     assert 'used 2 time' not in caplog.text
     assert authenticate(username=USERNAME, password=password) is not None
-    assert 'used 2 times' in caplog.text
+    assert 'used 2 time' in caplog.text
 
-    assert 'last time' not in caplog.text
+    assert 'used 1 time' not in caplog.text
     assert authenticate(username=USERNAME, password=password) is not None
-    assert 'last time' in caplog.text
+    assert 'used 1 time' in caplog.text
 
 
 def test_authenticate_ppolicy_pwdExpireWarning(slapd_ppolicy, settings, db, caplog):
-- 
2.25.1

