Skip to content

Commit

Permalink
[IMP] password_security: use ir.config_parameter
Browse files Browse the repository at this point in the history
Replace fields on res.company by ir.config_parameter
Remove dead test for v16 migration script
  • Loading branch information
alexis-via committed Nov 16, 2024
1 parent ce6c04d commit dc7b86d
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 153 deletions.
1 change: 0 additions & 1 deletion password_security/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).

from . import res_company
from . import res_config_settings
from . import res_users
from . import res_users_pass_history
46 changes: 0 additions & 46 deletions password_security/models/res_company.py

This file was deleted.

40 changes: 33 additions & 7 deletions password_security/models/res_config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,45 @@ class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

password_expiration = fields.Integer(
related="company_id.password_expiration", readonly=False
string="Days",
default=60,
config_parameter="password_security.expiration_days",
help="How many days until passwords expire",
)
password_minimum = fields.Integer(
related="company_id.password_minimum", readonly=False
string="Minimum Hours",
default=24,
config_parameter="password_security.minimum_hours",
help="Number of hours until a user may change password again",
)
password_history = fields.Integer(
related="company_id.password_history", readonly=False
string="History",
default=30,
config_parameter="password_security.history",
help="Disallow reuse of this many previous passwords - use negative "
"number for infinite, or 0 to disable",
)
password_lower = fields.Integer(
string="Lowercase",
default=1,
config_parameter="password_security.lower",
help="Require number of lowercase letters",
)
password_upper = fields.Integer(
string="Uppercase",
default=1,
config_parameter="password_security.upper",
help="Require number of uppercase letters",
)
password_lower = fields.Integer(related="company_id.password_lower", readonly=False)
password_upper = fields.Integer(related="company_id.password_upper", readonly=False)
password_numeric = fields.Integer(
related="company_id.password_numeric", readonly=False
string="Numeric",
default=1,
config_parameter="password_security.numeric",
help="Require number of numeric digits",
)
password_special = fields.Integer(
related="company_id.password_special", readonly=False
string="Special",
default=1,
config_parameter="password_security.special",
help="Require number of unique special characters",
)
96 changes: 55 additions & 41 deletions password_security/models/res_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,37 @@ def write(self, vals):
vals["password_write_date"] = fields.Datetime.now()
return super().write(vals)

@api.model
def _get_all_password_params(self):
params = self.env["ir.config_parameter"].sudo()
res = {
"minlength": int(
params.get_param("auth_password_policy.minlength", default=0)
),
"expiration_days": int(
params.get_param("password_security.expiration_days", default=60)
),
"minimum_hours": int(
params.get_param("password_security.minimum_hours", default=60)
),
"history": int(params.get_param("password_security.history", default=30)),
"lower": int(params.get_param("password_security.lower", default=1)),
"upper": int(params.get_param("password_security.upper", default=1)),
"numeric": int(params.get_param("password_security.numeric", default=1)),
"special": int(params.get_param("password_security.special", default=1)),
}
return res

@api.model
def get_password_policy(self):
data = super().get_password_policy()
company_id = self.env.user.company_id
pwd_params = self._get_all_password_params()
data.update(
{
"password_lower": company_id.password_lower,
"password_upper": company_id.password_upper,
"password_numeric": company_id.password_numeric,
"password_special": company_id.password_special,
"password_lower": pwd_params["lower"],
"password_upper": pwd_params["upper"],
"password_numeric": pwd_params["numeric"],
"password_special": pwd_params["special"],
}
)
return data
Expand All @@ -58,36 +79,31 @@ def _check_password_policy(self, passwords):

def password_match_message(self):
self.ensure_one()
company_id = self.company_id
message = []
if company_id.password_lower:
pwd_params = self._get_all_password_params()
if pwd_params["lower"]:
message.append(
_("\n* Lowercase letter (at least %s characters)")
% str(company_id.password_lower)
_("\n* Lowercase letter (at least %s characters)") % pwd_params["lower"]
)
if company_id.password_upper:
if pwd_params["upper"]:
message.append(
_("\n* Uppercase letter (at least %s characters)")
% str(company_id.password_upper)
_("\n* Uppercase letter (at least %s characters)") % pwd_params["upper"]
)
if company_id.password_numeric:
if pwd_params["numeric"]:
message.append(
_("\n* Numeric digit (at least %s characters)")
% str(company_id.password_numeric)
_("\n* Numeric digit (at least %s characters)") % pwd_params["numeric"]
)
if company_id.password_special:
if pwd_params["special"]:
message.append(
_("\n* Special character (at least %s characters)")
% str(company_id.password_special)
% pwd_params["special"]
)
if message:
message = [_("Must contain the following:")] + message

params = self.env["ir.config_parameter"].sudo()
minlength = params.get_param("auth_password_policy.minlength", default=0)
if minlength:
if pwd_params["minlength"]:
message = [
_("Password must be %d characters or more.") % int(minlength)
_("Password must be %d characters or more.") % pwd_params["minlength"]
] + message
return "\r".join(message)

Expand All @@ -100,16 +116,14 @@ def _check_password_rules(self, password):
self.ensure_one()
if not password:
return True
company_id = self.company_id
params = self.env["ir.config_parameter"].sudo()
minlength = params.get_param("auth_password_policy.minlength", default=0)
pwd_params = self._get_all_password_params()
password_regex = [
"^",
"(?=.*?[a-z]){" + str(company_id.password_lower) + ",}",
"(?=.*?[A-Z]){" + str(company_id.password_upper) + ",}",
"(?=.*?\\d){" + str(company_id.password_numeric) + ",}",
r"(?=.*?[\W_]){" + str(company_id.password_special) + ",}",
".{%d,}$" % int(minlength),
"(?=.*?[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):
raise ValidationError(self.password_match_message())
Expand All @@ -121,11 +135,12 @@ def _password_has_expired(self):
if not self.password_write_date:
return True

if not self.company_id.password_expiration:
pwd_params = self._get_all_password_params()
if not pwd_params["expiration_days"]:
return False

days = (fields.Datetime.now() - self.password_write_date).days
return days > self.company_id.password_expiration
return days > pwd_params["expiration_days"]

def action_expire_password(self):
expiration = delta_now(days=+1)
Expand All @@ -139,19 +154,19 @@ def _validate_pass_reset(self):
:raises: UserError on invalidated pass reset attempt
:return: True on allowed reset
"""
pwd_params = self._get_all_password_params()
for user in self:
pass_min = user.company_id.password_minimum
if pass_min <= 0:
if pwd_params["minimum_hours"] <= 0:
continue
write_date = user.password_write_date
delta = timedelta(hours=pass_min)
delta = timedelta(hours=pwd_params["minimum_hours"])
if write_date + delta > datetime.now():
raise UserError(
_(
"Passwords can only be reset every %d hour(s). "
"Please contact an administrator for assistance."
)
% pass_min
% pwd_params["minimum_hours"]
)
return True

Expand All @@ -160,20 +175,19 @@ def _check_password_history(self, password):
:raises: UserError on reused password
"""
crypt = self._crypt_context()
pwd_params = self._get_all_password_params()
for user in self:
password_history = user.company_id.password_history
if not password_history: # disabled
if not pwd_params["history"]: # disabled
recent_passes = self.env["res.users.pass.history"]
elif password_history < 0: # unlimited
elif pwd_params["history"] < 0: # unlimited
recent_passes = user.password_history_ids
else:
recent_passes = user.password_history_ids[:password_history]
recent_passes = user.password_history_ids[: pwd_params["history"]]
if recent_passes.filtered(
lambda r: crypt.verify(password, r.password_crypt)
):
raise UserError(
_("Cannot use the most recent %d passwords")
% user.company_id.password_history
_("Cannot use the most recent %d passwords") % pwd_params["history"]
)

def _set_encrypted_password(self, uid, pw):
Expand Down
1 change: 0 additions & 1 deletion password_security/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@
from . import test_password_history
from . import test_reset_password
from . import test_signup
from . import test_migration
2 changes: 1 addition & 1 deletion password_security/tests/test_change_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ def test_04_change_password_check_password_history(self):
"""It should fail when chosen password was previously used"""

# Set password history limit
self.env["ir.config_parameter"].sudo().set_param("password_security.history", 3)
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
user.company_id.password_history = 3
self.assertEqual(len(user.password_history_ids), 0)

# Change password: password history records created
Expand Down
8 changes: 6 additions & 2 deletions password_security/tests/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ def test_05_web_login_expire_pass(self):
env = self.env(cr)
user = env["res.users"].search([("login", "=", self.username)])
user.password_write_date = three_days_ago
user.company_id.password_expiration = 1
self.env["ir.config_parameter"].sudo().set_param(
"password_security.expiration_days", 1
)

# Try to log in
response = self.login(self.username, self.passwd)
Expand All @@ -110,7 +112,9 @@ def test_06_web_login_log_out_if_expired(self):
env = self.env(cr)
user = env["res.users"].search([("login", "=", self.username)])
user.password_write_date = three_days_ago
user.company_id.password_expiration = 1
self.env["ir.config_parameter"].sudo().set_param(
"password_security.expiration_days", 1
)

# Try to access just a page
req_page1 = self.url_open("/web")
Expand Down
33 changes: 0 additions & 33 deletions password_security/tests/test_migration.py

This file was deleted.

Loading

0 comments on commit dc7b86d

Please sign in to comment.