From 45930489edddc5b96a1c1866bacfb93f719a4b0a Mon Sep 17 00:00:00 2001 From: Ken Woychesko Date: Wed, 17 Jun 2026 05:47:09 -0400 Subject: [PATCH] [FIX] password_security: enforce configured per-character-class count The per-class requirements were built as e.g. `(?=.*?[A-Z]){N,}`, which applies the `{N,}` quantifier to a zero-width lookahead -- a no-op -- so any value >= 1 only ever required a single matching character. Setting e.g. Uppercase = 2 still accepted a password with one uppercase letter (same for lowercase, numeric and special). Move the quantifier inside the lookahead group -- `(?=(?:.*?[A-Z]){N,})` -- so the configured count is actually enforced. --- password_security/models/res_users.py | 8 ++++---- password_security/tests/test_res_users.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/password_security/models/res_users.py b/password_security/models/res_users.py index 8ee63b5aca..81a4056a6b 100644 --- a/password_security/models/res_users.py +++ b/password_security/models/res_users.py @@ -119,10 +119,10 @@ def _check_password_rules(self, password): pwd_params = self._get_all_password_params() password_regex = [ "^", - "(?=.*?[a-z]){" + str(pwd_params["lower"]) + ",}", - "(?=.*?[A-Z]){" + str(pwd_params["upper"]) + ",}", - "(?=.*?\\d){" + str(pwd_params["numeric"]) + ",}", - r"(?=.*?[\W_]){" + str(pwd_params["special"]) + ",}", + "(?=(?:.*?[a-z]){" + str(pwd_params["lower"]) + ",})", + "(?=(?:.*?[A-Z]){" + str(pwd_params["upper"]) + ",})", + "(?=(?:.*?\\d){" + str(pwd_params["numeric"]) + ",})", + r"(?=(?:.*?[\W_]){" + str(pwd_params["special"]) + ",})", ".{%d,}$" % pwd_params["minlength"], ] if not re.search("".join(password_regex), password): diff --git a/password_security/tests/test_res_users.py b/password_security/tests/test_res_users.py index 965beabacc..7959ee274c 100644 --- a/password_security/tests/test_res_users.py +++ b/password_security/tests/test_res_users.py @@ -149,6 +149,17 @@ def test_underscore_is_special_character(self): rec_id = self._new_record() rec_id._check_password("asdQWE12345_3") + def test_check_password_enforces_class_count(self): + """It should require the configured *count* of a character class""" + rec_id = self._new_record() + # Require two uppercase letters + self.env["ir.config_parameter"].sudo().set_param("password_security.upper", 2) + # Only one uppercase letter: must be rejected + with self.assertRaises(UserError): + rec_id._check_password("aSdqwe123$%^") + # Two uppercase letters: accepted + rec_id._check_password("aSDqwe123$%^") + def test_user_with_admin_rights_can_create_users(self): demo = self.env.ref("base.user_demo") demo.groups_id |= self.env.ref("base.group_erp_manager")